GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

커스텀 체크박스 9종 — 체크 값으로 바뀌는 아홉 가지 방식

커스텀 체크박스 아홉 종을 값 기준으로 나누면, 아홉 가지 중 하나는 체크 값이 켜지면 체크 표시의 선 길이 값이 0에서 끝까지 그려지고, 하나는 같은 값이 바뀌는 순간 박스의 크기 값이 잠깐 커졌다가 튕기듯 가라앉으며, 또 하나는 상위 박스의 값이 바뀌면 모든 하위 박스의 체크 값이

체크박스는 클릭 한 번으로 값이 두 상태(때로는 세 상태) 사이를 오가는 가장 단순한 입력이지만, 그 값을 화면에 어떻게 옮기느냐에 따라 결과는 전혀 다른 아홉 개의 장면이 됩니다. 아래는 그 계산을 하나씩 뜯어보고, html·scss·js 전체를 화면에서 직접 확인할 수 있게 정리한 목록입니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개의 해부 — 어떤 값이 언제 바뀌는가

아홉 항목 모두 실제 <input type="checkbox"> 하나를 화면 밖으로 숨긴 채(위치를 absolute로 빼고 opacity를 0으로) 그대로 두고, 옆의 장식용 상자에만 aria-hidden="true"를 달아 시각 효과를 맡깁니다. 값을 바꾸는 계산 방식은 셋으로 나뉩니다.

easeOutCubic 한 겹으로 미는 항목 — 01·04·06·08·09

01·04·06·08·09 다섯 항목은 같은 easeOutCubic(t) 함수를 각자의 값에 맞춰 씁니다. 01과 09는 체크 표시 선의 stroke-dashoffset 값을, 04는 배지 숫자의 세로 위치 값을, 06은 채움 원의 반지름 진행 값을, 08은 잠금 아이콘의 회전 각도 값을 이 함수로 보정합니다. 01과 09는 SVGGeometryElement.getTotalLength()로 선의 실제 길이를 재서 LEN에 담아 두기 때문에 짐작한 px가 아니라 실측 길이가 기준이고, 06은 card.getBoundingClientRect()로 카드의 실제 가로·세로를 읽어 대각선 길이를 구한 뒤 그 값을 채움 반지름의 목표로 삼습니다.

감쇠 진동으로 튀는 항목 — 03·07

03과 07은 Math.exp(-계수 * t)로 진폭을 줄여가며 cos·sin에 태우는 감쇠 진동 계산을 씁니다. 03은 체크 순간 박스 크기 값이 목표보다 커졌다가 잦아들고, 07은 제출 시도가 들어왔는데 필수 박스가 비어 있으면 가로 위치 값이 좌우로 흔들리다 멈춥니다. 같은 수식 구조를 쓰지만 03은 크기(transform: scale)에, 07은 위치(transform: translateX)에 적용한다는 점이 다릅니다.

체크박스 리스트 값을 그 자리에서 계산하는 항목 — 02·05

02와 05는 하위 여러 체크박스 리스트를 두고 상위 박스 하나가 그 합을 대표합니다. 02는 change 시점마다 체크된 개수를 전체 개수로 나눈 비율을 구해 parent.indeterminate를 세우고 그 비율을 배경 알파 값으로 바로 사영하며, 05는 transition-delay를 자식 인덱스 순서로 미리 심어 둔 채 값 자체는 한 번에 바꾸고 화면에 드러나는 시점만 계단처럼 늦춥니다. 두 항목 모두 프레임마다 JS가 값을 다시 계산하지 않고 CSS transition이 보정을 맡는다는 공통점이 있습니다.

측정으로 보면 07(누적면적 9.70%)이 아홉 개 중 가장 넓게 화면을 바꾸고 01(0.44%)이 가장 좁게 바꿉니다. 07은 흔들리는 동안 행 전체와 안내문 색까지 함께 바뀌어 픽셀 변화 범위가 넓어지고, 01은 얇은 선 하나의 stroke-dashoffset만 움직이니 좁은 게 당연합니다.

항목 누적면적% 강도_평균 움직인프레임
01 체크 선 그리기 0.44 73.0 14/23
02 부분선택 대시 1.70 48.4 15/23
03 스프링 튐 0.54 82.7 11/23
04 선택 개수 롤 0.77 83.4 13/23
05 전체선택 계단 2.43 50.6 20/23
06 색 채움 와이프 7.92 199.6 8/23
07 필수 오류 흔들림 9.70 60.3 16/23
08 비활성 흐림 고정 1.17 44.1 7/23
09 모션 축소 대응 비교 0.62 96.7 11/23

09만 따로 볼 필요가 있습니다. 이 항목은 하나의 체크 값 변화를 일반 판과 축소 판 두 패널에 나란히 그려서, 같은 입력이 matchMedia의 두 분기로 나뉘는 순간을 직접 대조하도록 만든 유일한 항목입니다.

패턴별 코드

01체크 선 그리기 전환 — 체크 값이 켜지면 체크 표시의 선 길이 값이 0에서 끝까지 순서대로 그려지는 방식

체크 값이 켜지는 순간 체크 표시의 선 길이 값이 0에서 끝까지 그려지고, 꺼지면 반대 순서로 지워집니다. LEN은 getTotalLength()로 잰 실제 경로 길이이고, currentOffset()이 진행 중이던 값에서 새 목표까지를 easeOutCubic으로 이어받습니다.

  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }

  function currentOffset(ts) {
    if (!drawing) return drawTo;
    var t = Math.min(1, (ts - drawStart) / DRAW_MS);
    return drawFrom + (drawTo - drawFrom) * easeOutCubic(t);
  }

  function setChecked(next, ts) {
    input.checked = next;
    box.classList.toggle('cbd__box--on', next);
    drawFrom = currentOffset(ts);
    drawTo = next ? 0 : LEN;
    drawStart = ts;
    drawing = true;
  }
체크 선 그리기 전환 — 체크 값이 켜지면 체크 표시의 선 길이 값이 0에서 끝까지 순서대로 그려지는 방식
새 창에서 열기

01 · 체크 선 그리기 전환 — 체크 값이 켜지면 체크 표시의 선 길이 값이 0에서 끝까지 순서대로 그려지는 방식 — 측정 누적면적 0.442% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

DRAW_MSstroke-dashoffsetgetTotalLength

02부분선택 대시 전환 — 하위 항목이 일부만 선택되면 상위 박스의 표시 값이 대시 모양으로 바뀌는 방식

하위 선택 개수 값이 전체와 0 사이에 있으면 상위 박스의 표시 값이 체크도 빈칸도 아닌 대시로 바뀝니다. paintParent()는 change가 올 때마다 비율을 다시 구해 네이티브 indeterminate 속성을 세우고, 그 비율을 --cbi-alpha라는 CSS 변수로 넘겨 배경 색 진하기를 직접 정합니다.

  function paintParent() {
    paintChildren();
    var checkedCount = children.filter(function (c) { return c.checked; }).length;
    var ratio = checkedCount / children.length; // 계산 겹: 선택 비율 값
    var all = ratio === 1, none = ratio === 0;
    parent.checked = all;
    parent.indeterminate = !all && !none;
    parentBox.classList.toggle('cbi__box--on', all);
    parentBox.classList.toggle('cbi__box--mixed', !all && !none);
    // 계산 겹: 선택 비율을 배경 알파로 직접 사영한다(MIX_ALPHA 를 바닥으로 두고 남은 폭을 ratio 만큼 채움)
    var alpha = none ? 0 : (MIX_ALPHA + (1 - MIX_ALPHA) * ratio);
    parentBox.style.setProperty('--cbi-alpha', alpha.toFixed(3));
  }
부분선택 대시 전환 — 하위 항목이 일부만 선택되면 상위 박스의 표시 값이 대시 모양으로 바뀌는 방식
새 창에서 열기

02 · 부분선택 대시 전환 — 하위 항목이 일부만 선택되면 상위 박스의 표시 값이 대시 모양으로 바뀌는 방식 — 측정 누적면적 1.703% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

indeterminateMIX_ALPHAbackground

03스프링 튐 전환 — 체크 값이 바뀌는 순간 박스의 크기 값이 목표보다 크게 튀었다가 튕기듯 가라앉는 방식

체크 값이 바뀌는 순간 박스의 크기 값이 목표치를 살짝 넘겨 커졌다가 두세 번 튕기며 원래 크기로 가라앉습니다. popScale()은 SPRING_ZETA로 진폭을 감쇠시키며 SCALE_OVERSHOOT만큼 cos 곡선을 따라 넘치고 되돌아오는 값을 매 프레임 계산합니다.

  function popScale(t) {
    // 계산 겹: 감쇠 코사인 진동 — t=0 에서 SCALE_OVERSHOOT 만큼 튀어 오른 뒤 1로 잦아든다
    if (t < 0 || t > POP_MS) return 1;
    var amp = Math.exp(-SPRING_ZETA * (t / POP_MS));
    return 1 + amp * SCALE_OVERSHOOT * Math.cos(POP_FREQ * t);
  }

  function setChecked(next, ts) {
    input.checked = next;
    box.classList.toggle('cbs__box--on', next);
    popStart = ts;
  }
스프링 튐 전환 — 체크 값이 바뀌는 순간 박스의 크기 값이 목표보다 크게 튀었다가 튕기듯 가라앉는 방식
새 창에서 열기

03 · 스프링 튐 전환 — 체크 값이 바뀌는 순간 박스의 크기 값이 목표보다 크게 튀었다가 튕기듯 가라앉는 방식 — 측정 누적면적 0.544% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPRING_ZETASCALE_OVERSHOOTtransform

04선택 개수 롤 전환 — 선택된 개수 값이 오르내릴 때마다 배지 숫자의 자리 값이 위아래로 굴러가듯 바뀌는 방식

선택 개수 값이 하나 늘거나 줄 때마다 배지 숫자의 세로 위치 값이 다음 숫자 쪽으로 굴러가듯 바뀝니다. paint()가 체크된 개수를 셀 때마다 -count * ROW_H를 새 목표로 두고, 목표가 바뀐 순간에만 rollStart를 다시 찍어 currentPos()가 이전 값에서부터 이어받게 합니다.

  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }

  function currentPos(ts) {
    if (!rolling) return to;
    var t = Math.min(1, (ts - rollStart) / ROLL_MS);
    return from + (to - from) * easeOutCubic(t);
  }

  function paint(ts) {
    inputs.forEach(function (inp, i) { boxes[i].classList.toggle('cbc__box--on', inp.checked); });
    var count = inputs.filter(function (i) { return i.checked; }).length;
    var target = -count * ROW_H;
    if (target !== to) {
      from = currentPos(ts);
      to = target;
      rollStart = ts;
      rolling = true;
    }
  }
선택 개수 롤 전환 — 선택된 개수 값이 오르내릴 때마다 배지 숫자의 자리 값이 위아래로 굴러가듯 바뀌는 방식
새 창에서 열기

04 · 선택 개수 롤 전환 — 선택된 개수 값이 오르내릴 때마다 배지 숫자의 자리 값이 위아래로 굴러가듯 바뀌는 방식 — 측정 누적면적 0.765% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ROLL_MStranslateYclip-path

05전체선택 계단 전환 — 상위 박스의 전체선택 값이 켜지면 하위 박스들의 체크 값이 순서대로 짧은 간격을 두고 바뀌는 방식

상위 전체선택 값이 켜지는 순간 하위 박스들의 체크 값이 위에서부터 짧은 간격을 두고 차례로 바뀝니다. 인덱스마다 transition-delay를 STAGGER_MS씩 벌려 미리 심어 둔 뒤, 체크 값 자체는 한 번에 바꾸고 화면에 드러나는 시점만 CSS가 계단처럼 늦춥니다.

  // 계산 겹: 각 자식 박스의 transition-delay 를 인덱스 × STAGGER_MS 로 만든다 — 값 자체는
  // 동시에 바뀌지만 화면에 드러나는 시점이 계단처럼 순서를 갖는다
  childBoxes.forEach(function (b, i) {
    b.style.setProperty('transition-delay', (i * STAGGER_MS) + 'ms');
  });

  function paintParent() {
    var count = childInputs.filter(function (c) { return c.checked; }).length;
    var all = count === childInputs.length, none = count === 0;
    parent.checked = all;
    parent.indeterminate = !all && !none;
    parentBox.classList.toggle('cba__box--on', all);
    parentBox.classList.toggle('cba__box--mixed', !all && !none);
  }
전체선택 계단 전환 — 상위 박스의 전체선택 값이 켜지면 하위 박스들의 체크 값이 순서대로 짧은 간격을 두고 바뀌는 방식
새 창에서 열기

05 · 전체선택 계단 전환 — 상위 박스의 전체선택 값이 켜지면 하위 박스들의 체크 값이 순서대로 짧은 간격을 두고 바뀌는 방식 — 측정 누적면적 2.429% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAGGER_MSindeterminatetransition-delay

06색 채움 와이프 전환 — 체크 값이 켜지면 박스의 배경 채움 값이 한쪽 모서리에서 반대쪽으로 번지듯 바뀌는 방식

체크 값이 켜지는 순간 박스의 채움 값이 한쪽 모서리에서 시작해 반대쪽 모서리까지 번져나갑니다. measure()가 카드의 실제 가로·세로로 대각선 길이를 구해 두고, easeOutCubic으로 보정한 진행 값을 그 대각선에 곱해 채움 원의 반지름으로 씁니다.

  function measure() {
    // 계산 겹: 카드 실측 대각선 길이 — 채움 원이 반대쪽 모서리에 정확히 닿는 반지름 값
    var r = card.getBoundingClientRect();
    diag = Math.sqrt(r.width * r.width + r.height * r.height);
  }

  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }

  function human() {
    if (isDemo) { root.classList.remove('is-demo'); isDemo = false; }
  }

  function setChecked(next, ts) {
    input.checked = next;
    wipeDir = next ? 1 : -1;
    wipeStart = ts;
    wiping = true;
  }
색 채움 와이프 전환 — 체크 값이 켜지면 박스의 배경 채움 값이 한쪽 모서리에서 반대쪽으로 번지듯 바뀌는 방식
새 창에서 열기

06 · 색 채움 와이프 전환 — 체크 값이 켜지면 박스의 배경 채움 값이 한쪽 모서리에서 반대쪽으로 번지듯 바뀌는 방식 — 측정 누적면적 7.924% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WIPE_MSclip-pathbackground

07필수 오류 흔들림 전환 — 필수 박스가 비어 있는데 제출을 시도하면 박스의 가로 위치 값이 짧게 좌우로 흔들리는 방식

제출 시도 값이 들어오는 순간 빈 필수 박스의 가로 위치 값이 짧게 좌우로 흔들리고 테두리 색 값이 붉게 바뀝니다. shakeOffset()은 03과 같은 감쇠 진동 구조를 위치 값에 적용하고, triggerError()는 클릭이 올 때마다 shakeStart를 다시 찍어 진폭을 처음부터 새로 시작시킵니다.

  function shakeOffset(t) {
    // 계산 겹: 감쇠 사인 진동 — 시간이 지날수록 진폭이 줄어드는 흔들림 값을 직접 계산
    var amp = Math.exp(-SHAKE_ZETA * (t / SHAKE_MS));
    return SHAKE_AMP * amp * Math.sin(SHAKE_FREQ * t);
  }

  function triggerError() {
    row.classList.add('cbe__row--error');
    hint.classList.add('cbe__hint--error');
    shakeStart = performance.now();
    shaking = true;
  }
필수 오류 흔들림 전환 — 필수 박스가 비어 있는데 제출을 시도하면 박스의 가로 위치 값이 짧게 좌우로 흔들리는 방식
새 창에서 열기

07 · 필수 오류 흔들림 전환 — 필수 박스가 비어 있는데 제출을 시도하면 박스의 가로 위치 값이 짧게 좌우로 흔들리는 방식 — 측정 누적면적 9.7% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SHAKE_MStranslateXborder-color

08비활성 흐림 고정 전환 — 비활성 값이 켜지면 박스의 밝기 값이 낮아지고 체크 값이 더는 바뀌지 않는 방식

비활성 값이 켜지는 순간 박스의 밝기 값이 낮아지고, 그 뒤로는 클릭이 와도 체크 값이 바뀌지 않습니다. apply()가 input.disabled와 aria-pressed를 같은 순간에 세우고, 옆에 있는 잠금 아이콘의 회전 각도 값은 currentAngle()이 easeOutCubic으로 이어받습니다.

  function currentAngle(ts) {
    if (!animating) return angleTo;
    var t = Math.min(1, (ts - animStart) / LOCK_MS);
    return angleFrom + (angleTo - angleFrom) * easeOutCubic(t);
  }

  function apply(nextLocked, ts) {
    locked = nextLocked;
    input.disabled = locked;
    row.classList.toggle('cbl__row--locked', locked);
    toggle.setAttribute('aria-pressed', String(locked));
    toggleText.textContent = locked ? '잠금 해제' : '잠금';
    angleFrom = currentAngle(ts);
    angleTo = locked ? 0 : UNLOCK_ANGLE;
    animStart = ts;
    animating = true;
  }
비활성 흐림 고정 전환 — 비활성 값이 켜지면 박스의 밝기 값이 낮아지고 체크 값이 더는 바뀌지 않는 방식
새 창에서 열기

08 · 비활성 흐림 고정 전환 — 비활성 값이 켜지면 박스의 밝기 값이 낮아지고 체크 값이 더는 바뀌지 않는 방식 — 측정 누적면적 1.173% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

disabledfilterpointer-events

09모션 축소 대응 비교 전환 — 같은 체크 값 변화를 일반 판은 선 길이 값을 그려서, 축소 판은 중간 단계 없이 바로 보여주는 방식

같은 체크 값이 바뀔 때 일반 판은 선 길이 값이 그려지듯 바뀌고, 축소 판은 중간 단계 없이 목표 값으로 바로 바뀝니다. setState() 한 함수 안에서 일반 판은 01과 같은 currentOffset() 보간을 타고, 축소 판은 같은 next 값을 받아 클래스만 바로 뒤집어 두 갈래를 나란히 보여줍니다.

  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }

  function currentOffset(ts) {
    if (!drawing) return drawTo;
    var t = Math.min(1, (ts - drawStart) / DURATION_MS);
    return drawFrom + (drawTo - drawFrom) * easeOutCubic(t);
  }

  function setState(next, ts) {
    // 일반 판: DURATION_MS 동안 draw-on 을 실측 길이 기준으로 보간(matchMedia 가 false 일 때의 실제 경로)
    normalInput.checked = next;
    normalBox.classList.toggle('cbr__box--on', next);
    drawFrom = currentOffset(ts);
    drawTo = next ? 0 : LEN;
    drawStart = ts;
    drawing = true;

    // 축소 판: matchMedia 결과와 같은 논리 분기로, 중간 프레임 없이 즉시 목표 상태로 점프
    reducedInput.checked = next;
    reducedBox.classList.toggle('cbr__box--on', next);
  }
모션 축소 대응 비교 전환 — 같은 체크 값 변화를 일반 판은 선 길이 값을 그려서, 축소 판은 중간 단계 없이 바로 보여주는 방식
새 창에서 열기

09 · 모션 축소 대응 비교 전환 — 같은 체크 값 변화를 일반 판은 선 길이 값을 그려서, 축소 판은 중간 단계 없이 바로 보여주는 방식 — 측정 누적면적 0.617% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

DURATION_MSCYCLE_MSmatchMedia

값이 모양과 색을 바꾸는 곳 — 체크 값·부분선택 값·개수 값 기준

아홉 항목을 관통하는 기준은 체크 값 하나가 아니라 셋입니다. 체크 값 자체(01·03·06·07·08·09)는 선 길이·크기·채움·위치·밝기·비교 화면을 바꾸고, 부분선택 값(02·05)은 하위 리스트를 대표하는 상위 박스의 표시를 바꾸고, 개수 값(04)은 배지 숫자의 위치를 바꿉니다. input 체크박스 커스텀 작업을 할 때 이 셋 중 무엇을 기준으로 삼을지 먼저 정해 두면 어떤 계산 함수가 필요한지도 자연히 정해집니다. 같은 체크 값이라도 그 값을 받는 쪽이 선 길이(01)인지 크기(03)인지 채움 반지름(06)인지에 따라 계산에 필요한 실측 값의 종류도 달라지는데, 01·09는 경로 길이를, 06은 카드의 가로·세로를 직접 재서 씁니다.

아홉 패턴의 html·scss·js 소스는 각 항목마다 통째로 열어볼 수 있습니다.

어디서 깨지는가 — 빠른 연속 토글, 필수 항목, 모션 감소

07은 제출 버튼을 빠르게 여러 번 누르면 triggerError()가 클릭마다 shakeStart를 새로 찍어 진폭이 채 가라앉기 전에 다시 처음 진폭으로 튀어 오릅니다. 그 결과 한 번의 정착 애니메이션이 아니라 클릭 간격이 짧을수록 거의 끊기지 않는 잔진동처럼 보이는데, 이건 required 속성을 가진 유일한 항목인 07이 감쇠 진동을 매 클릭마다 원점에서 다시 시작하기 때문입니다. 화면에는 결과만 보이고 실제 코드는 비번 ukycae87 으로 열리는 zip 안에 들어 있어서, 이 07의 감쇠 사인 식과 앞서 본 06의 실측 대각선 계산을 나란히 펼쳐 두고 비교하며 값을 바꿔볼 수 있습니다.

모션 감소 대응은 항목마다 셋으로 나뉩니다. 01·03·04·06·07·08은 각자의 지속시간 상수를 낮춰 보간 구간을 눈에 보이지 않을 만큼 짧게 만들고, 02·05는 원래도 값 자체를 한 번에 바꾸고 CSS transition만 관여하므로 그 지속시간을 줄이면 그대로 해결되며, 09는 matchMedia의 두 분기를 애초에 화면에 나란히 그려 두어 축소 여부를 비교 자체로 보여줍니다.

값 자체를 옮기는 계산은 034 CSS 토글 스위치나 076 사실적인 토글 스위치에서도 등장하지만, 그 두 편은 손잡이 하나가 두 자리 사이를 오가는 위치 값에 집중합니다. 이 편은 체크·부분선택·개수라는 서로 다른 세 값이 각각 선 길이·배경 알파·세로 위치로 흩어져 나가는 쪽을 다루므로, 계산의 출발점이 됩니다.

부분선택 값이 상위로 올라가는 계산은 254 폼 검증 UI의 오류 표시 흐름과 성격이 반대입니다. 254는 입력 칸 하나가 틀렸을 때 그 결과를 같은 칸 곁의 안내문 한 줄로만 내려보내는 낱개 판정을 주로 다루는 반면, 이 편의 02·05는 하위 리스트에 흩어진 여러 체크 값들이 다시 상위 박스 하나로 모여 비율 하나로 요약되는 계산만 떼어서 본다는 점이 다릅니다.

변형 세 가지와 접근성

값 하나를 바꿔 보면 감쇠·와이프 계산이 눈에 어떻게 다르게 읽히는지 바로 비교할 수 있습니다.

이름 값 느낌
스프링 오버슛 배율 키우기 03 SCALE_OVERSHOOT 0.34 → 0.55 오버슛 값을 0.34→0.55 로 올리면 체크 박스가 목표 크기를 훨씬 크게 넘겨 튀었다가 가라앉아 반응이 더 장난스러워진다.
필수 오류 흔들림 폭 줄이기 07 SHAKE_AMP 8 → 3 흔들림 폭을 8→3px 로 줄이면 빈 필수 박스의 경고가 훨씬 은근해져 화면이 덜 소란스러워진다.
색 채움 와이프 속도 늦추기 06 WIPE_MS 420 → 900 채움 시간을 420→900ms 로 늘리면 배경이 모서리에서부터 번져가는 과정이 눈에 뚜렷하게 보여 체크 순간의 무게감이 커진다.

아홉 항목 모두 진짜 <input type="checkbox">를 화면 밖으로 숨겨 두고 옆의 장식 상자는 aria-hidden으로 가려서, 값이 바뀌는 순간 보조기기가 읽는 것은 항상 이 숨겨진 원본 입력의 네이티브 체크·부분선택 상태입니다. 그중 08은 잠금 버튼의 aria-pressed 값을 apply() 안에서 상태가 바뀔 때마다 직접 다시 세우는 유일한 항목이라, css 체크박스 커스텀 작업을 할 때 클릭형 토글 버튼을 곁들이는 구성이라면 이 방식을 참고할 만합니다.

FAQ

체크 표시를 svg 선 대신 png 아이콘으로 바꿔도 되나요?

01과 09는 path.getTotalLength()로 선의 실제 길이를 재서 stroke-dashoffset을 보간하는 방식이라 svg path가 있어야 동작합니다. png 같은 래스터 아이콘으로 바꾸려면 선이 그려지는 연출 대신 투명도나 클립 값으로 등장을 표현해야 하며, 체크박스 이미지 커스텀을 준비할 때는 이 둘을 같은 자리에 그대로 넣지 않는 편이 안전합니다.

부분선택 값은 상위에서 하위로도 내려가나요, 하위에서 상위로만 올라가나요?

02와 05 모두 실제로 값은 두 방향 다 돕니다. 자식 체크박스 리스트의 상태가 바뀔 때마다 개수를 다시 세어 상위 박스의 checked·indeterminate를 정하는 계산이 기본이고, 상위 박스를 직접 클릭했을 때는 반대로 그 값이 모든 자식에게 그대로 내려가는 별도 처리가 붙습니다.

제출 버튼을 아주 빠르게 여러 번 누르면 흔들림이 어떻게 되나요?

07의 triggerError()는 호출될 때마다 shakeStart를 그 시점으로 다시 찍기 때문에, 클릭 간격이 감쇠가 끝나는 시간보다 짧으면 흔들림이 매번 최대 진폭에서 다시 시작합니다. 화면에서는 한 번의 짧은 흔들림이 아니라 누르는 동안 계속 흔들리는 것처럼 보이며, 이는 오류가 아니라 소스에 그대로 있는 동작입니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.