04Mask value toggle — when the masking value turns on, all but the first few digit values switch to a symbol value
When the mask value turns on, the later digit values switch to a symbol value and hide, and turning it off shows the original digit values again.
Best forA saved-card list's number display, card-number guidance in a payment history
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">Toggle mask value</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 Mask value toggle — when the masking value turns on, all but the first few digit values switch to a symbol value — Measured area 0.732% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · Credit Card Input UI: 9 Patterns
The post this demo lives in
Credit Card Input UI: 9 PatternsCredit card input UI: One of nine spaces digits into groups of four, one recalculates a validity value live as digits fi See all demos in this post
If you like this effect - similar demos
Inputs05 Select-all cascade toggle — when the parent box's select-all value turns on, each child box's checked value flips in order with a short staggerCSS Custom Checkbox: 9 Interaction Patterns
Inputs08 Unit toggle flip — the card folds over and the display value switches at the 90-degree point once the unit enters a changed stateNumber Stepper UI: 9 Interaction Patterns
Inputs02 Character-class count fill — the strength value is set by how many distinct character-type values are mixed into the inputPassword Strength Meter UI: 9 Calculation Patterns