01자릿수 값 그룹 전환 — 입력된 숫자 값이 네 자리마다 그 사이 간격 값이 자동으로 생기는 방식
입력된 숫자 값이 네 자리에 이를 때마다 그 사이 간격 값이 자동으로 생겨 번호가 그룹으로 나뉩니다.
쓰임결제 폼의 카드 번호 입력 칸, 회원 카드 등록 화면
digitsBeforegroupsOfpaintTrack

<main class="stage">
<div class="ci is-demo" id="ci">
<label class="ci__field">
<input class="ci__input" id="ciInput" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" maxlength="19" value="">
</label>
<div class="ci__track" id="ciTrack" aria-hidden="true">
<span class="ci__seg" data-g="0"><span class="ci__segFill"></span></span>
<span class="ci__gap" data-gap="0"></span>
<span class="ci__seg" data-g="1"><span class="ci__segFill"></span></span>
<span class="ci__gap" data-gap="1"></span>
<span class="ci__seg" data-g="2"><span class="ci__segFill"></span></span>
<span class="ci__gap" data-gap="2"></span>
<span class="ci__seg" data-g="3"><span class="ci__segFill"></span></span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 커서 위치 값을 digit count 로 환산해 간격 값 삽입 개수만큼 보정한다(index.html).
@include stage($stage-ink);
.ci {
width: 100%;
max-width: 320px;
display: grid;
gap: $sp-4;
}
.ci__field { display: block; }
.ci__input {
width: 100%;
box-sizing: border-box;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 0;
background: $subject-cream;
color: $subject-ink;
font-weight: 600;
font-size: 18px;
line-height: 1.3;
font-family: "SFMono-Regular", Consolas, monospace;
letter-spacing: .06em;
&:focus { outline: 2px solid $subject-blue-light; outline-offset: 2px; }
}
.ci__track {
display: flex;
align-items: center;
gap: $sp-1;
}
.ci__seg {
flex: 1;
height: $sp-1;
border-radius: $r-xs;
background: rgba($subject-cream, .16);
overflow: hidden;
}
.ci__segFill {
display: block;
width: 100%;
height: 100%;
border-radius: $r-xs;
background: $subject-blue-light;
transform: scaleX(0);
transform-origin: left center;
transition: transform $dur-quick $ease-out;
}
.ci__gap {
width: $sp-1;
height: $sp-1;
border-radius: 50%;
background: rgba($subject-cream, .16);
transform: scale(.4);
opacity: 0;
transition: transform $dur-quick $ease-elastic, opacity $dur-quick $ease-out, background-color $dur-quick $ease-out;
}
.ci__gap--on {
opacity: 1;
transform: scale(1);
background: $subject-blue-light;
}
@media (max-width: 340px) {
.ci__input { padding: $sp-2 $sp-3; font-size: 16px; }
}
@media (prefers-reduced-motion: reduce) {
.ci__segFill, .ci__gap { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ci {
width: 100%;
max-width: 320px;
display: grid;
gap: 16px;
}
.ci__field {
display: block;
}
.ci__input {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border-radius: 12px;
border: 0;
background: #fff7e6;
color: #17141a;
font-weight: 600;
font-size: 18px;
line-height: 1.3;
font-family: "SFMono-Regular", Consolas, monospace;
letter-spacing: 0.06em;
}
.ci__input:focus {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.ci__track {
display: flex;
align-items: center;
gap: 4px;
}
.ci__seg {
flex: 1;
height: 4px;
border-radius: 4px;
background: rgba(255, 247, 230, 0.16);
overflow: hidden;
}
.ci__segFill {
display: block;
width: 100%;
height: 100%;
border-radius: 4px;
background: #5b8cf8;
transform: scaleX(0);
transform-origin: left center;
transition: transform 220ms ease-out;
}
.ci__gap {
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(255, 247, 230, 0.16);
transform: scale(0.4);
opacity: 0;
transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 220ms ease-out, background-color 220ms ease-out;
}
.ci__gap--on {
opacity: 1;
transform: scale(1);
background: #5b8cf8;
}
@media (max-width: 340px) {
.ci__input {
padding: 8px 12px;
font-size: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.ci__segFill, .ci__gap {
transition: none;
}
}(function () {
var root = document.getElementById('ci');
var input = document.getElementById('ciInput');
var track = document.getElementById('ciTrack');
var segFills = track.querySelectorAll('.ci__segFill');
var gaps = track.querySelectorAll('.ci__gap');
// 계산 겹: 자릿수 값을 4개씩 묶어 다시 조립하고, 커서 위치 값을 digit count 로
// 환산해 삽입된 간격 값 개수만큼 보정한다 — 그냥 재대입하면 커서가 맨 끝으로 튄다.
function digitsBefore(str, upto) { return str.slice(0, upto).replace(/\D/g, '').length; }
function groupsOf(rawDigits) {
var g = rawDigits.match(/.{1,4}/g) || [];
while (g.length < 4) g.push('');
return g;
}
function paintTrack(rawDigits) {
var g = groupsOf(rawDigits);
for (var i = 0; i < 4; i++) {
segFills[i].style.transform = 'scaleX(' + (g[i].length / 4) + ')';
}
for (var j = 0; j < 3; j++) {
var on = rawDigits.length > (j + 1) * 4;
gaps[j].classList.toggle('ci__gap--on', on);
}
}
function reformat(prevCaret) {
var prevDigits = digitsBefore(input.value, prevCaret);
var rawDigits = input.value.replace(/\D/g, '').slice(0, 16);
var groups = rawDigits.match(/.{1,4}/g) || [];
input.value = groups.join(' ');
// 커서 위치 보정: 앞선 자릿수 값 개수 + 그 앞에 이미 생긴 간격 값 개수(4자리마다 1개)
var spacesBefore = Math.floor(Math.max(prevDigits - 1, 0) / 4);
var newCaret = Math.min(prevDigits + spacesBefore, input.value.length);
input.setSelectionRange(newCaret, newCaret);
paintTrack(rawDigits);
}
input.addEventListener('input', function () {
root.classList.remove('is-demo');
stopAutoplay();
reformat(input.selectionStart);
});
// 자동 루프: 시드 고정 자릿수 값을 한 자리씩 늘렸다 줄인다.
var FULL = '9999999999999995';
var SEQ = [];
for (var i = 0; i <= FULL.length; i++) SEQ.push(FULL.slice(0, i));
for (var k = FULL.length - 1; k >= 0; k--) SEQ.push(FULL.slice(0, k));
var si = 0;
var autoTimer = null;
function autoTick() {
var rawDigits = SEQ[si];
var groups = rawDigits.match(/.{1,4}/g) || [];
input.value = groups.join(' ');
paintTrack(rawDigits);
si = (si + 1) % SEQ.length;
}
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(autoTick, 160); autoTick(); }
else { autoTick(); }
})();import { useEffect, useRef } from "react";
import styles from "./AutoSpaceGroups.module.scss";
type Box<T> = { current: T };
export default function AutoSpaceGroups() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const input = root.querySelector(`.${styles.input}`) as HTMLInputElement | null;
const segFills = root.querySelectorAll(`.${styles.segFill}`);
const gaps = root.querySelectorAll(`.${styles.gap}`);
if (!input || segFills.length === 0) return;
// 계산 겹: 자릿수 값을 4개씩 묶어 다시 조립하고, 커서 위치 값을 digit count 로
// 환산해 삽입된 간격 값 개수만큼 보정한다 — 그냥 재대입하면 커서가 맨 끝으로 튄다.
function digitsBefore(str: string, upto: number) {
return str.slice(0, upto).replace(/\D/g, "").length;
}
function groupsOf(rawDigits: string) {
const g: string[] = rawDigits.match(/.{1,4}/g) || [];
while (g.length < 4) g.push("");
return g;
}
function paintTrack(rawDigits: string) {
const g = groupsOf(rawDigits);
for (let i = 0; i < 4; i++) {
(segFills[i] as HTMLElement).style.transform = "scaleX(" + g[i].length / 4 + ")";
}
for (let j = 0; j < 3; j++) {
const on = rawDigits.length > (j + 1) * 4;
gaps[j].classList.toggle(styles["gap--on"], on);
}
}
function reformat(prevCaret: number) {
const prevDigits = digitsBefore(input!.value, prevCaret);
const rawDigits = input!.value.replace(/\D/g, "").slice(0, 16);
const groups = rawDigits.match(/.{1,4}/g) || [];
input!.value = groups.join(" ");
// 커서 위치 보정: 앞선 자릿수 값 개수 + 그 앞에 이미 생긴 간격 값 개수(4자리마다 1개)
const spacesBefore = Math.floor(Math.max(prevDigits - 1, 0) / 4);
const newCaret = Math.min(prevDigits + spacesBefore, input!.value.length);
input!.setSelectionRange(newCaret, newCaret);
paintTrack(rawDigits);
}
function onInput() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
reformat(input!.selectionStart || 0);
}
input.addEventListener("input", onInput);
// 자동 루프: 시드 고정 자릿수 값을 한 자리씩 늘렸다 줄인다.
const FULL = "9999999999999995";
const SEQ: string[] = [];
for (let i = 0; i <= FULL.length; i++) SEQ.push(FULL.slice(0, i));
for (let k = FULL.length - 1; k >= 0; k--) SEQ.push(FULL.slice(0, k));
let si = 0;
let autoTimer: number | null = null;
function autoTick() {
const rawDigits = SEQ[si];
const groups = rawDigits.match(/.{1,4}/g) || [];
input!.value = groups.join(" ");
paintTrack(rawDigits);
si = (si + 1) % SEQ.length;
}
function stopAutoplay() {
if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; }
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(autoTick, 160); autoTick(); }
else { autoTick(); }
return () => {
stopAutoplay();
input.removeEventListener("input", onInput);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.ci} ${styles["is-demo"]}`}>
<label className={styles.field}>
<input className={styles.input} type="text" inputMode="numeric" autoComplete="off" spellCheck={false} maxLength={19} defaultValue="" />
</label>
<div className={styles.track} aria-hidden="true">
<span className={styles.seg}><span className={styles.segFill} /></span>
<span className={styles.gap} />
<span className={styles.seg}><span className={styles.segFill} /></span>
<span className={styles.gap} />
<span className={styles.seg}><span className={styles.segFill} /></span>
<span className={styles.gap} />
<span className={styles.seg}><span className={styles.segFill} /></span>
</div>
</div>
);
}01 자릿수 값 그룹 전환 — 입력된 숫자 값이 네 자리마다 그 사이 간격 값이 자동으로 생기는 방식 — 측정 누적면적 0.912% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식
이 데모가 속한 글
카드 번호 입력 UI 9종 — 자릿수 값마다 반응이 달라지는 아홉 가지 방식카드 번호 입력 ui는 자릿수 값이 채워지는 상태에 따라 화면 여러 곳의 값이 함께 바뀌는 인터랙션인데, 아홉 개 중 하나는 입력된 숫자 값이 네 자리에 이를 때마다 그 사이 간격 값이 자동으로 생기고, 하나는 자릿 이 글의 데모 전부 보기
이 효과가 마음에 들면 — 비슷한 데모
입력 UI04 선택 개수 롤 전환 — 선택된 개수 값이 오르내릴 때마다 배지 숫자의 자리 값이 위아래로 굴러가듯 바뀌는 방식커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식
입력 UI08 단위 플립 전환 — 단위가 바뀐 상태가 되면 카드가 뒤집히며 90도 지점에서 표시 값이 바뀌는 스텝퍼숫자 스텝퍼 UI 패턴 9종 — 길게 누를수록 빨라지는 것부터 눈금에 붙는 것까지
입력 UI08 확인 값 실시간 대조 — 확인 입력 값과 원래 값이 갈라지는 위치 값을 실시간으로 찾는 방식비밀번호 강도 UI 9종 — 강도 값이 계산되는 아홉 가지 방식