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

CSS 토글 버튼 9종 — 실물처럼 만드는 토글 스위치 UI

css 토글 버튼은 화면 속에서 전원을 넣고 끄는 작은 스위치를 뜻합니다. 아홉 개를 라이브러리 없이 체크박스 하나로 만들었고, 아래 격자에서 지금 돌아갑니다. 클릭·Space·드래그 전부 실제로 움직입니다.

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

순서는 인기순이 아니라 스위치가 실물이 되어 가는 순서입니다. 먼저 눌림과 빛이 생기고(01), 유리 광택이 입혀지고(02), 성격이 붙고(03), 무광 재질로 차분해지고(04), 두 칸을 고르는 법이 생기고(05), 빛이 흐르고(06), 숨어 있던 띠가 드러나고(07), 체크로 확정되고(08), 마지막에 손끝을 따라오는 관성이 붙습니다(09). 여덟 개는 role="switch" 와 aria-checked 로, 두 칸을 고르는 05 만 aria-pressed 로 상태를 알리고, 색이 아니라 위치·글자·빛으로도 켜짐을 읽게 했습니다. 같은 button 기둥에서 레버·다이얼 같은 부품을 다룬 CSS 스위치 버튼 9종과 평면 토글의 기본기를 모은 CSS 토글 스위치 9종가 이 편의 이웃입니다.

01전원 버튼과 LED 링

동심원 베벨을 겹겹이 쌓은 둥근 버튼이 눌리면 아래로 들어가고, 테두리의 전원 기호 링에 빛이 번지는 기기 제어판용 주 전원입니다.

전원 버튼과 LED 링
새 창에서 열기

01 · 전원 버튼과 LED 링 — 측정 누적면적 5.355% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

box-shadowtranslateYaria-checked

켜짐과 꺼짐의 시간이 다르다

// LED 링 — 켜질 때는 캡 복귀 뒤 $dur-quick 늦게 $dur-base 에 걸쳐 번지고,
// 꺼질 때는 지연 없이 빠르게 사그라든다(방향마다 transition 규칙을 다르게 둔 한 방향 트릭).
.pw__led {
  position: absolute; inset: 22px; z-index: 2; border-radius: 50%; pointer-events: none;
  box-shadow: inset 0 0 0 3px rgba($color, .14);
  transition: box-shadow $dur-quick $ease-out;
}
.pw.is-on .pw__led {
  box-shadow: inset 0 0 0 3px $color, 0 0 16px rgba($color, .6), 0 0 34px rgba($color, .28);
  transition: box-shadow $dur-base $ease-out $duration;
}

기본 규칙은 꺼짐 방향(짧고 지연 없음)을 담당하고, .is-on 규칙이 켜짐 방향(버튼이 돌아온 뒤 220ms 부터 300ms 에 걸쳐 번짐)을 덮어써서, 상태마다 delay 를 다르게 두는 것만으로 실물의 비대칭이 만들어집니다.

눌림은 그림자가 함께 깊어진다

.pw__cap:active {
  transform: translateY(4px) scale(.98);
  box-shadow: 0 2px 4px rgba(0, 0, 0, .6), inset 0 1px 0 rgba($subject-cream, .22), inset 0 -2px 4px rgba(0, 0, 0, .6);
}

캡이 4px 내려가는 동시에 바깥 그림자가 움츠러드는데, 눌림 깊이는 눈보다 손이 먼저 알아채는 정보라 transform 과 box-shadow 를 같은 시간에 묶었습니다.

02유리 광택 세로 토글 두 개

세로로 선 홈 안을 유리 광택 노브가 위아래로 미끄러지는, 장비 패널의 채널 두 줄입니다 — 두 개가 서로 반대 상태로 놓여 0 과 1 을 나란히 견줍니다.

유리 광택 세로 토글 두 개
새 창에서 열기

02 · 유리 광택 세로 토글 두 개 — 측정 누적면적 10.045% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

linear-gradienttranslateY:checked

체크박스 스스로 상태를 가른다

.ch__input:checked ~ .ch__track .ch__knob {
  transform: translateY(var(--travel));
  box-shadow: 0 9px 14px rgba($stage-ink, .34), inset 0 -3px 6px rgba($stage-ink, .28), inset 0 1px 0 rgba($stage-paper, .9);
}

입력은 <input type="checkbox" role="switch"> 하나라 토글용 JS 가 0줄이고, :checked 형제 선택자가 노브 위치·그림자·숫자 강조를 한꺼번에 정하며 Space 도 체크박스가 알아서 받습니다.

유리는 스펙큘러 한 점으로 읽힌다

.ch__knob {
  position: absolute; left: 9px; top: 9px; width: 46px; height: 46px; border-radius: 50%;
  background: linear-gradient(180deg, rgba($stage-paper, .95), rgba($color, .6) 45%, rgba($stage-paper, .35) 78%, rgba($subject-ink, .22));
  box-shadow: 0 3px 5px rgba($stage-ink, .4), inset 0 -3px 6px rgba($stage-ink, .28), inset 0 1px 0 rgba($stage-paper, .9);
  transform: translateY(0);
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.ch__knob::before {
  content: ""; position: absolute; left: 16%; top: 12%; width: 44%; height: 32%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba($stage-paper, .95), rgba($stage-paper, 0));
}

세로 그라디언트에 좌상단 타원 스펙큘러 하나만 얹으면 유리로 읽히고, 내려앉은 상태의 그림자가 뜬 상태보다 멀리 번지는 폭이 홈 바닥에 닿았다는 단서입니다.

03굴러가며 눈을 뜨는 토글

노브가 미끄러지는 대신 한 바퀴 굴러서 반대편으로 가고, 도착하면 감겨 있던 두 눈이 벌어지는 아이 대상 앱의 소리 토글입니다.

굴러가며 눈을 뜨는 토글
새 창에서 열기

03 · 굴러가며 눈을 뜨는 토글 — 측정 누적면적 9.99% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotatescaleYcubic-bezier

이동과 회전을 두 겹으로 나눈다

// tr__knob 은 이동만, tr__roll 은 회전만 — 눈(tr__eyes)은 회전에서 빼 둥근 채 도착하게 한다.
.tr__knob {
  position: absolute; left: 9px; top: 8px; width: 69px; height: 69px;
  transform: translateX(0);
  transition: transform $duration $easing;
}
.tr__roll {
  position: absolute; inset: 0; border-radius: 50%;
  background: linear-gradient(160deg, $color, rgba($color, .88) 45%, rgba($subject-ink, .16) 92%);
  box-shadow: 0 4px 8px rgba($stage-ink, .45), inset 0 2px 2px rgba($stage-paper, .8), inset 0 -3px 5px rgba($stage-ink, .22);
  transform: rotate(0deg);
  transition: transform $duration $easing;
}
.tr.is-on .tr__knob { transform: translateX(var(--travel)); }
.tr.is-on .tr__roll { transform: rotate(var(--roll)); }

바깥 겹은 translateX 로 163px 만큼 가고 안쪽 겹은 같은 시간에 271도 를 도는데, 회전각은 이동거리 ÷ 반지름의 근사값이라 딱 맞지 않아도 두 움직임이 같은 시간에 끝나면 바퀴처럼 읽힙니다.

눈은 회전 축에서 빼 둔다

.tr__eye {
  width: 8px; height: 8px; border-radius: 50%; background: $subject-ink;
  transform: scaleY(0);
  transition: transform $dur-quick $ease-pop;
}
.tr.is-on .tr__eye { transform: scaleY(1); }

눈은 회전하는 겹이 아니라 그 위의 한 겹에 두어 노브가 굴러도 똑바로 선 채 도착하고, 감김은 scaleY(0) 의 가는 선이라 회전과 겹쳐도 형태가 부서지지 않습니다.

04무광 키캡 토글 세 개

정사각에 가까운 키캡 세 개가 나란히 누워 있고, 누르면 아래로 내려앉으며 캡이 파랑으로 채워지는 도구 막대의 켜고 끄는 버튼입니다.

무광 키캡 토글 세 개
새 창에서 열기

04 · 무광 키캡 토글 세 개 — 측정 누적면적 7.817% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

box-shadowbackground-colortransition

눌림과 색 충전의 시간을 갈라 놓는다

.eq__key {
  display: flex; align-items: center; justify-content: center;
  width: 62px; height: 62px; margin: 0; padding: 0;
  border: 0; border-radius: $r-control; cursor: pointer;
  color: $subject-ink; // 아이콘(currentColor) 은 키에서 물려받는다 — eqIcon 키프레임이 이 color 를 움직인다
  background-color: $cap-off;
  box-shadow: $cap-rest-shadow;
  transform: translateY(0);
  transition: transform $dur-instant $easing, box-shadow $dur-instant $easing, background-color $duration $ease-out, color $duration $ease-out;
}
.eq__key.is-on { background-color: $color; color: $subject-cream; }

눌림(transform·box-shadow)은 140ms, 색 충전은 220ms 로 갈라 탁 눌리고 색이 뒤따라 차오르는 순서를 만들었습니다.

세 키는 같은 키프레임에 음수 지연만 다르게

.eq.is-demo .eq__key {
  animation-name: eqPress, eqFill, eqIcon;
  animation-duration: $dur-loop;
  animation-timing-function: $easing, steps(1, end), steps(1, end);
  animation-iteration-count: infinite;
}
.eq.is-demo .eq__key:nth-child(2) { animation-delay: calc(#{$dur-loop} / -3 * 2); }
.eq.is-demo .eq__key:nth-child(3) { animation-delay: calc(#{$dur-loop} / -3); }

키프레임은 한 벌로 두고 animation-delay 를 한 주기의 3분의 1씩 음수로 밀어 세 키가 차례로 깜빡이는데, 음수 지연은 시작 전 구간이 없어 채움 규칙도 필요 없습니다.

05두 칸 사이를 미끄러지는 선택 알약

긴 알약 트랙 안에서 짙은 알약이 두 라벨 사이를 미끄러지고, 올라앉은 쪽 글자만 색이 뒤집히는, 일·주 같은 기간을 고르는 자리입니다.

두 칸 사이를 미끄러지는 선택 알약
새 창에서 열기

05 · 두 칸 사이를 미끄러지는 선택 알약 — 측정 누적면적 8.249% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-template-columnstranslateXsteps(1, end)

칸은 균등 격자, 이동은 고정 거리

.sg__track {
  --travel: 116px; // 칸 폭 = 트랙 240 − 좌우 패딩 8 을 2 로 나눈 값. 좁은 화면에서는 96px
  display: grid; grid-template-columns: repeat(2, 1fr);
  position: relative;
  width: 240px; height: 48px; padding: $sp-1;
  border-radius: $r-pill;
  background: $subject-blue-deep;
}
.sg.is-week .sg__knob { transform: translateX(var(--travel)); }

인디케이터가 고정 px 로 움직이므로 두 칸은 repeat(2, 1fr) 균등 격자로 짰고, 칸을 flex: 1 1 0 으로 두면 렌더 폭이 고정값과 어긋나 알약이 라벨을 빗나가므로 좁은 화면에서는 --travel 만 줄입니다.

금속 면은 위가 밝고 아래가 어두운 한 겹으로

.sg__knob {
  position: absolute; left: 8px; top: 4px;
  width: 108px; height: 40px; border-radius: $r-pill;
  background: linear-gradient(180deg, rgba($subject-cream, .16), rgba($subject-cream, .02) 34%, rgba(0, 0, 0, .38)), $subject-ink;
  box-shadow: inset 0 1px 0 rgba($subject-cream, .3), 0 4px 10px rgba(0, 0, 0, .5);
  transform: translateX(0);
  transition: transform $duration $easing;
}
.sg__cell[aria-pressed="true"] { color: $subject-cream; }

알약 노브는 잉크 바탕에 위쪽 광택 한 겹과 양 끝 세로 그립인데, 칸은 <button aria-pressed> 라 탭과 엔터로도 전환되고 노브가 도착하면 그 칸의 글자만 진해집니다.

06빛이 쓸려 가는 유리 토글

노브가 건너가는 동안 유리 표면의 빛덩이가 사선으로 쓸려 가고 별 점들이 밝아지는, 어두운 화면 한가운데의 큰 야간 모드 스위치입니다.

빛이 쓸려 가는 유리 토글
새 창에서 열기

06 · 빛이 쓸려 가는 유리 토글 — 측정 누적면적 11.507% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

radial-gradienttranslateXbox-shadow

빛은 비어 있는 트랙 위에 겹친다

.onx__light {
  position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(120% 150% at 32% 50%, rgba($color, .96), rgba($subject-blue-deep, .92) 62%, rgba($subject-blue-deep, .8));
  opacity: 0;
  transition: opacity $dur-enter $ease-out;
}
.onx.is-on .onx__light { opacity: 1; }

꺼짐에는 트랙이 비어 있고 켜지면 밝은 중심을 둔 방사 그라디언트가 600ms 에 걸쳐 차오르는데, 트랙·빛·노브가 각각 다른 층이라 색이 스위치를 침범하지 않습니다.

반사광은 반대편으로 천천히 밀린다

.onx__sweep {
  position: absolute; left: 44%; top: 12%; width: 52%; height: 76%;
  border-radius: 50%; pointer-events: none;
  background: linear-gradient(52deg, rgba($stage-paper, 0) 34%, rgba($stage-paper, .46) 56%, rgba($stage-paper, 0) 78%);
  transform: translateX(0) rotate(0deg);
  transition: transform $dur-enter $ease-out;
}
.onx.is-on .onx__sweep { transform: translateX(-76%) rotate(-9deg); }

사선 띠 반사광이 켜지는 순간 왼쪽으로 밀리며 9도 기울어 광원이 이동한 것처럼 읽히고, 별 점 다섯 개는 도착 시점에 밝아지며 노란 무대 위 상태 글자는 잉크로 대비를 지켰습니다.

07밀어 젖히는 무음 스위치

손잡이를 끝까지 밀면 홈 안쪽에 숨어 있던 주황 띠가 드러나는, 알림 끄기처럼 되돌리기 쉬운 차단 스위치입니다.

밀어 젖히는 무음 스위치
새 창에서 열기

07 · 밀어 젖히는 무음 스위치 — 측정 누적면적 4.199% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

insettranslateXsteps(1, end)

숨어 있던 띠가 상태를 말한다

.sm__rail {
  position: relative; box-sizing: border-box;
  width: 124px; height: 48px; border-radius: $r-pill;
  background: linear-gradient(180deg, rgba($subject-ink, .95), rgba($subject-ink, .78));
  box-shadow: inset 0 3px 6px rgba($stage-ink, .9), inset 0 -1px 0 rgba($subject-cream, .22);
}
.sm__band {
  position: absolute; left: 4px; top: 4px; width: 56px; height: 40px;
  border-radius: $r-pill;
  background: linear-gradient(180deg, $color, rgba($stage-ink, .55));
  box-shadow: inset 0 2px 3px rgba($stage-ink, .5);
}

띠는 처음부터 레일 안쪽에 깔려 있고 손잡이가 덮고 있을 뿐이라, 손잡이가 끝으로 가면 그 자리가 드러나 무음을 색이 아니라 「드러남」으로 알립니다.

끝 칸에서 한 번 튕겨 붙는다

.sm__handle {
  --travel: 56px; // 손잡이가 반대쪽 끝 칸까지 가는 거리. 좁은 화면에서는 아래 미디어쿼리가 줄인다
  transition: transform $duration $easing;
}
.sm.is-muted .sm__handle { transform: translateX(var(--travel)); }

$ease-pop 베지에의 오버슈트가 손잡이를 끝 칸보다 지나쳤다가 도로 붙이는데, 하드웨어 슬라이드는 중간에 서지 않으므로 이동은 한 번의 전환으로 끝내고 --travel 변수로 좁은 화면에서도 같은 성격을 유지합니다.

08체크로 굳는 확인 토글

노브가 반대쪽에 도착한 다음에야 체크 선이 한 획으로 그려지는, 약관 동의처럼 확정이 중요한 자리의 토글입니다.

체크로 굳는 확인 토글
새 창에서 열기

08 · 체크로 굳는 확인 토글 — 측정 누적면적 3.276% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stroke-dashoffsetanimation-delaypolyline

체크는 SVG 한 획으로 그린다

<button class="cm__switch" id="cmSwitch" type="button" role="switch" aria-checked="false" aria-label="약관 동의">
  <span class="cm__knob" aria-hidden="true">
    <svg class="cm__check" viewBox="0 0 24 24" aria-hidden="true">
      <polyline points="5,12.5 10,17.5 19,7.5"></polyline>
    </svg>
  </span>
</button>

막대 두 개로 체크를 흉내 내면 출발점이 어긋나 사선 한 줄로 보이기 쉬운데, SVG polyline 하나의 stroke-dashoffset 을 26에서 0으로 줄이면 꺾이는 지점이 정확한 한 획이 됩니다.

이동이 끝난 뒤에 그리기가 시작된다

.cm__knob {
  position: absolute; left: 4px; top: 4px; display: grid; place-items: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: $stage-paper;
  box-shadow: 0 2px 4px rgba($subject-ink, .34);
  transform: translateX(0);
  transition: transform $duration $easing;
}
.cm.is-agreed .cm__knob { transform: translateX(var(--travel)); }
.cm.is-agreed .cm__check polyline { stroke-dashoffset: 0; transition-delay: $duration; }

「도착」과 「확인」은 다른 신호라 체크의 transition 에 transition-delay: $duration 을 두어 노브가 다 도착한 뒤에야 선이 그려지고, 끌 때는 체크가 먼저 지워집니다.

09끌었다 놓는 관성 토글

노브를 손가락으로 끌면 손끝을 따라오고, 절반을 넘긴 채 놓으면 남은 거리를 관성으로 달려가 붙는, 실수로 켜지면 곤란한 잠금 해제 스위치입니다.

끌었다 놓는 관성 토글
새 창에서 열기

09 · 끌었다 놓는 관성 토글 — 측정 누적면적 5.107% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointerdownsetPointerCapturetranslateX

끄는 동안만 전환을 끈다

.dt.is-dragging .dt__knob { transition: none; cursor: grabbing; }
.dt.is-dragging .dt__fill { transition: none; }

드래그 중에는 손끝과 노브가 어긋나면 안 되므로 .is-dragging 이 transition 을 끄고, 손을 놓으면 클래스가 떨어져 전환이 돌아오고, 그때 $ease-pop 이 남은 거리를 달립니다.

절반을 넘겼는지로 자리를 정한다

function settle(open) {
  dtKnob.style.transform = '';
  dtKnob.setAttribute('aria-checked', open ? 'true' : 'false');
  dtRoot.classList.toggle('is-open', open);
  offset = open ? travel() : 0;
}
function onUp() {
  if (!dragging) return;
  dragging = false;
  dtRoot.classList.remove('is-dragging');
  // 절반을 넘겼으면 남은 거리를 관성으로 마저 달리고, 못 넘겼으면 제자리로 돌아온다
  settle(offset > travel() / 2);
}

놓은 지점이 절반을 넘었는지만 보고, 포인터는 setPointerCapture 로 묶어 트랙 밖으로 손이 나가도 끌기가 끊기지 않게 하며 클릭·Space 도 같은 settle() 을 불러 키보드만으로 잠금이 풀립니다.

css 토글 스위치는 체크박스 하나로 어디까지 되나?

여기까지가 답입니다. 상태 저장은 체크박스나 aria-checked 하나에 두고, 재질은 그라디언트와 box-shadow 몇 겹으로, 성격은 이징과 시간 배분으로 만들면 라이브러리 없이도 전원 버튼부터 관성 토글까지 전부 css 스위치로 살아납니다. 다만 셋만은 규칙이 다릅니다 — 03 의 회전·08 의 체크 그리기·09 의 드래그는 상태가 아니라 움직임이 본체라, 미리보기 루프와 실제 조작 경로를 반드시 따로 검증해야 합니다.

어디서 깨지는가 — 격자에서만 도는 스위치

드래그 직후의 click 이 상태를 뒤집었다

09 의 첫 판을 Playwright 로 30px 끌었다 놓았더니 aria-checked 가 false 에서 true 로 바뀌어 있었습니다. 끌기가 끝난 자리에서 브라우저가 같은 요소에 click 이벤트를 하나 더 쏘았고, 토글 핸들러가 그것을 새 클릭으로 읽은 것입니다. 6px 넘게 움직였다는 사실을 기억해 뒀다가 click 에서 되돌리는 두 줄을 넣어 고쳤습니다. 측정은 미리보기 루프로만 통과하므로, 조작 경로가 있는 스위치는 반드시 손으로 재야 합니다.

자식의 색 선언이 부모의 애니메이션을 죽였다

04 는 아이콘 색을 .eq__icon { color: $subject-ink } 으로 직접 정해 두었는데, 세 번째 키의 아이콘이 미리보기 루프 내내 켜짐 색인 채로 굳어 있었습니다. 애니메이션은 키(부모)의 color 를 움직이는데 자식의 직접 선언이 상속을 이겨버린 것입니다. 색을 키로 옮겨 물려받게 하니 세 키가 제 갈 길로 돌아왔습니다. 계산값을 프레임마다 찍어보기 전까지 소스 어디에도 오류는 없었습니다.

반쯤 찬 색이 대표 이미지로 뽑혔다

04 의 대표 장면을 픽셀로 견줘보니 캡이 회색과 파랑 사이 반쯤 물든 프레임이 뽑혀 있었고, 그 위의 어두운 아이콘이 옅게 떠 보였습니다. 색 채움을 이징으로 두는 동안 반톤 구간이 몇 프레임씩 존재한 탓이라, 캡과 아이콘 색을 같은 지점에 steps(1, end) 컷으로 함께 뒤집었습니다. 실제 클릭은 정적 규칙의 220ms 페이드 그대로라 손맛은 변하지 않습니다. 압축 파일 아홉 개의 비밀번호는 3hntnd6x 이고, 아홉 개 전부 클릭·Space·드래그 조작을 수치로 남겨 두었습니다.

변형 세 가지와 접근성

값 세 개를 바꾸는 것만으로 스위치 아홉 개의 온도가 한꺼번에 움직입니다.

이름 바꾼 값 느낌
duration $dur-base 300ms → $dur-instant 140ms 노브가 미끄러지지 않고 기계 부품처럼 딱 붙습니다
easing $ease-spring → $ease-pop 노브가 반대쪽 끝에서 한 번 튕겼다가 자리를 잡습니다
color $subject-blue-deep → $stage-orange 켜짐 색이 파랑에서 주황으로 바뀌어 경고 스위치처럼 읽힙니다

여덟 개는 role="switch" 를 달았고(02 는 체크박스가 aria-checked 를 native 로 노출), 두 칸 고르기인 05 는 aria-pressed 를 씁니다. 상태는 색만이 아니라 노브 위치·상태 글자·띠의 드러남으로 겹쳐 알리므로 색을 구별하기 어려운 눈에도 켜짐이 읽힙니다. 09 는 포인터와 키보드 두 경로가 같은 settle() 로 끝나고, prefers-reduced-motion: reduce 에서는 미리보기 루프가 멈추고 스위치가 가장 잘 읽히는 꺼짐 위치 한 장이 남습니다.

더 읽을거리: CSS 스위치 버튼 9종 은 레버·로커·다이얼 부품을, 버튼 애니메이션 가이드는 시간과 이징의 기준을 다룹니다. 명세는 MDN switch role 문서와 :checked 문서를 보면 됩니다.

FAQ

토글 스위치 ui 를 구현할 때 버튼과 체크박스 중 무엇을 쓰나요

상태를 스스로 저장할 필요가 있으면 체크박스에 role="switch" 를 얹습니다(02 번). Space 처리와 폼 제출이 공짜로 따라옵니다. 상태를 앱이 따로 관리하면 버튼에 aria-checked 를 직접 유지합니다(01·07·08·09 번).

아이콘 스위치는 색만으로 켜짐을 알려도 되나요

04 번처럼 색이 주된 단서라도 눌림 깊이와 아이콘 색 반전을 함께 두면 안전합니다. 색각 이상인 눈에는 파랑-회색 구별이 약할 수 있으니, 가능하면 위치나 글자 같은 두 번째 단서를 하나 더 얹는 편이 좋습니다.

드래그 토글은 터치 화면에서도 되나요

됩니다. 09 번은 setPointerCapture 로 마우스·터치·펜을 한 경로로 받고, touch-action: none 으로 끌기 중 화면이 스크롤되지 않게 막았습니다. 다만 손가락이 정확히 절반을 넘기기 어려울 때는 놓은 방향이 아니라 절반 규칙이 의도대로 굴러가는지 손으로 확인해 보세요.

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

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