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 원작 · zip 에 SCSS·React 포함 · 카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식
이 데모가 속한 글
카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식카드 번호 입력 ui는 자릿수 값이 채워지는 상태에 따라 화면 여러 곳의 값이 함께 바뀌는 인터랙션인데, 아홉 개 중 하나는 입력된 숫자 값이 네 자리에 이를 때마다 그 사이 간격 값이 자동으로 생기고, 하나는 자릿 이 글의 데모 전부 보기
이 효과가 마음에 들면 — 비슷한 데모
입력 UI05 전체선택 계단 전환 — 상위 박스의 전체선택 값이 켜지면 하위 박스들의 체크 값이 순서대로 짧은 간격을 두고 바뀌는 방식커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식
입력 UI08 단위 플립 전환 — 단위가 바뀐 상태가 되면 카드가 뒤집히며 90도 지점에서 표시 값이 바뀌는 스텝퍼숫자 스텝퍼 UI 패턴 9종 — 길게 누를수록 빨라지는 것부터 눈금에 붙는 것까지
입력 UI02 문자 종류 수 채움 — 입력 값 안에 섞인 서로 다른 문자 종류 값의 개수에 따라 강도 값이 정해지는 방식비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식