04マスク値切替 — マスクの値がオンになると先頭の数桁を残し残りの桁の値が記号値に変わる方式
マスクの値がオンになると後ろの桁の値が記号の値に変わって隠れ、マスクの値がオフになると元の数字の値が再び見えます。
使いどころ保存済みカード一覧の番号表示、決済履歴のカード番号案内
STAGGER_MSKEEPDIGITS

<main class="stage">
<div class="cm is-demo" id="cm">
<div class="cm__row" id="cmRow"></div>
<button type="button" class="cm__toggle" id="cmToggle" aria-pressed="false">マスク値切替</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 가림 값 전환이 자릿수 값 순서대로 STAGGER_MS 씩 늦게 퍼진다(index.html --i, transition-delay).
@include stage($stage-paper);
.cm {
width: 100%;
max-width: 320px;
display: grid;
gap: $sp-5;
justify-items: center;
}
.cm__row {
display: flex;
align-items: center;
background: $subject-ink;
border-radius: $r-control;
padding: $sp-4;
box-sizing: border-box;
width: 100%;
}
.cm__group {
position: relative;
width: 13px;
height: 20px;
margin-right: 0;
&--gap { margin-left: $sp-2; }
}
.cm__digit,
.cm__mask {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: "SFMono-Regular", Consolas, monospace;
font-weight: 600;
font-size: 16px;
color: $subject-cream;
transition: transform $dur-quick $ease-spring, opacity $dur-quick $ease-out;
transition-delay: calc(var(--i, 0) * 45ms);
}
.cm__digit--fixed { position: static; transition: none; }
.cm__mask {
opacity: 0;
transform: scale(.4);
}
.cm--masked {
.cm__digit:not(.cm__digit--fixed) { opacity: 0; transform: scale(.4); }
.cm__mask { opacity: 1; transform: scale(1); }
}
.cm__toggle {
border: 0;
border-radius: $r-pill;
padding: $sp-2 $sp-5;
background: $subject-ink;
color: $subject-cream;
font-weight: 700;
font-size: 13px;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.95); }
&:focus-visible { outline: 2px solid $subject-blue-deep; outline-offset: 2px; }
}
@media (max-width: 340px) {
.cm__row { padding: $sp-3; }
.cm__group { width: 11px; }
.cm__digit, .cm__mask { font-size: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.cm__digit, .cm__mask { transition: none; transition-delay: 0ms; }
.cm__toggle { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cm {
width: 100%;
max-width: 320px;
display: grid;
gap: 20px;
justify-items: center;
}
.cm__row {
display: flex;
align-items: center;
background: #17141a;
border-radius: 12px;
padding: 16px;
box-sizing: border-box;
width: 100%;
}
.cm__group {
position: relative;
width: 13px;
height: 20px;
margin-right: 0;
}
.cm__group--gap {
margin-left: 8px;
}
.cm__digit,
.cm__mask {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: "SFMono-Regular", Consolas, monospace;
font-weight: 600;
font-size: 16px;
color: #fff7e6;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 220ms ease-out;
transition-delay: calc(var(--i, 0) * 45ms);
}
.cm__digit--fixed {
position: static;
transition: none;
}
.cm__mask {
opacity: 0;
transform: scale(0.4);
}
.cm--masked .cm__digit:not(.cm__digit--fixed) {
opacity: 0;
transform: scale(0.4);
}
.cm--masked .cm__mask {
opacity: 1;
transform: scale(1);
}
.cm__toggle {
border: 0;
border-radius: 999px;
padding: 8px 20px;
background: #17141a;
color: #fff7e6;
font-weight: 700;
font-size: 13px;
cursor: pointer;
transition: transform 140ms ease-out;
}
.cm__toggle:active {
transform: scale(0.95);
}
.cm__toggle:focus-visible {
outline: 2px solid #0a4fe8;
outline-offset: 2px;
}
@media (max-width: 340px) {
.cm__row {
padding: 12px;
}
.cm__group {
width: 11px;
}
.cm__digit, .cm__mask {
font-size: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.cm__digit, .cm__mask {
transition: none;
transition-delay: 0ms;
}
.cm__toggle {
transition: none;
}
}(function () {
var root = document.getElementById('cm');
var row = document.getElementById('cmRow');
var btn = document.getElementById('cmToggle');
var DIGITS = '9999999999999995';
var KEEP = 4; // 앞의 몇 자리 값은 항상 그대로
var STAGGER_MS = 45; // 계산 겹: 자릿수 값마다 이만큼씩 늦게 전환된다
var cells = [];
DIGITS.split('').forEach(function (ch, i) {
var group = document.createElement('span');
group.className = 'cm__group';
if (i > 0 && i % 4 === 0) group.classList.add('cm__group--gap');
if (i < KEEP) {
group.innerHTML = '<span class="cm__digit cm__digit--fixed">' + ch + '</span>';
} else {
var idx = i - KEEP;
group.style.setProperty('--i', idx);
group.innerHTML =
'<span class="cm__digit">' + ch + '</span>' +
'<span class="cm__mask">•</span>';
cells.push(group);
}
row.appendChild(group);
});
var masked = false;
function paint() {
root.classList.toggle('cm--masked', masked);
btn.setAttribute('aria-pressed', String(masked));
}
function toggle() {
masked = !masked;
paint();
}
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
stopAutoplay();
toggle();
});
var autoTimer = null;
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(toggle, 1500); }
else { masked = true; paint(); }
})();import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./MaskToggleReveal.module.scss";
type Box<T> = { current: T };
const DIGITS = "9999999999999995";
const KEEP = 4; // 앞의 몇 자리 값은 항상 그대로
const STAGGER_MS = 45; // 계산 겹: 자릿수 값마다 이만큼씩 늦게 전환된다
export default function MaskToggleReveal() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const btn = root.querySelector(`.${styles.toggle}`) as HTMLButtonElement | null;
if (!btn) return;
let masked = false;
function paint() {
root!.classList.toggle(styles["root--masked"], masked);
btn!.setAttribute("aria-pressed", String(masked));
}
function toggle() { masked = !masked; paint(); }
function onClick() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
toggle();
}
btn.addEventListener("click", onClick);
let autoTimer: number | null = null;
function stopAutoplay() {
if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; }
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(toggle, 1500); }
else { masked = true; paint(); }
return () => {
stopAutoplay();
btn.removeEventListener("click", onClick);
};
}, []);
const cells = DIGITS.split("").map((ch, i) => ({
ch,
gapBefore: i > 0 && i % 4 === 0,
fixed: i < KEEP,
idx: i - KEEP,
}));
return (
<div ref={rootRef} className={`${styles.stage} ${styles.cm} ${styles["is-demo"]}`}>
<div className={styles.row}>
{cells.map((c, i) => (
<span key={i} className={`${styles.group} ${c.gapBefore ? styles["group--gap"] : ""}`}>
{c.fixed ? (
<span className={`${styles.digit} ${styles["digit--fixed"]}`}>{c.ch}</span>
) : (
<>
<span className={styles.digit} style={{ transitionDelay: `${c.idx * STAGGER_MS}ms` } as CSSProperties}>{c.ch}</span>
<span className={styles.mask} style={{ transitionDelay: `${c.idx * STAGGER_MS}ms` } as CSSProperties}>•</span>
</>
)}
</span>
))}
</div>
<button type="button" className={styles.toggle} aria-pressed="false">가림 값 전환</button>
</div>
);
}04 マスク値切替 — マスクの値がオンになると先頭の数桁を残し残りの桁の値が記号値に変わる方式 — 計測 累積面積 0.732% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · カード番号 入力 UI 9選 — 桁ごとに反応が変わる9つの方式
このデモが属する記事
カード番号 入力 UI 9選 — 桁ごとに反応が変わる9つの方式カード番号 入力 UI:9つのうち1つは入力された数字値が4桁に達するたびその間の間隔値が自動的に生まれ、1つは桁の値が埋まるたびに番号全体の有効性値が再計算されて表示値が変わり、もう1つはポインターの位置の値が変わるとカードの絵の傾きの値 この記事のデモをすべて見る


