CSS 토글 스위치 9종 — 한 줄 복붙으로
CSS 토글 스위치는 <input type="checkbox"> 하나로 실제 켜고 끌 수 있으면서 트랙과 노브의 위치·색·아이콘만 다르게 꾸민 컨트롤입니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 기본 슬라이드
- 02 iOS 스타일
- 03 ON/OFF 라벨 내장
- 04 아이콘 전환
- 05 눌림 스퀴시 효과
- 06 배경색 채움 전환
- 07 해/달 아이콘 토글
- 08 크기 변형 믹스인
- 09 비활성 상태 스타일
아홉 개는 화려한 순서가 아니라 트랙 위에서 실제로 무엇을 바꾸는가 순으로 놓았습니다. 위치만 옮기는 기본형(01~02)에서 시작해, 트랙 안쪽 내용이 바뀌는 것(03~04), 눌림이나 채움 같은 연출이 들어간 것(05~07)을 지나, 구조 자체가 여럿이거나 아예 손댈 수 없는 두 개(08~09)로 끝납니다. 코드 블록엔 각 스위치의 핵심 선택자만 남겼고, 그대로 붙이면 돌아가는 완결본은 zip 안에 있습니다.
01기본 슬라이드
체크박스에 checked가 걸리면 원형 노브가 트랙 오른쪽까지 미끄러지고, 트랙은 두 겹을 겹쳐 배경색만 opacity로 바꿉니다. 설정 화면의 옵션 스위치나 체크박스를 대체할 가장 기본형입니다.
.switch__bg--off { background: rgba(255, 255, 255, .18); opacity: 1; }
.switch__bg--on { background: $color; opacity: 0; }
.switch__knob { transform: translateX(0); transition: transform $duration $easing; }
.switch__input:checked ~ .switch__track .switch__bg--off { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__bg--on { opacity: 1; }
.switch__input:checked ~ .switch__track .switch__knob { transform: translateX(24px); }
02iOS 스타일
iOS 설정 앱처럼 둥근 트랙과 흰 노브 조합에, 노브가 도착 지점에서 스프링 이징으로 한 번 더 튕기고 멈춥니다. 모바일 웹 설정 화면처럼 손끝의 반응감이 중요한 자리에 씁니다.
@keyframes knob-loop {
0%, 15% { transform: translateX(0); }
38% { transform: translateX(28px); } // 스프링 오버슈트
40%, 60% { transform: translateX(24px); }
85%, 100% { transform: translateX(0); }
}
03ON/OFF 라벨 내장
트랙 양 끝에 ON·OFF 글자를 고정해 두고, 노브가 이동하며 반대쪽 글자를 z-index로 덮습니다. 관리자 대시보드의 상태 스위치처럼 지금 값이 뭔지 글자로도 보여야 하는 곳에 씁니다.
.switch__text--on { left: 10px; }
.switch__text--off { right: 10px; }
.switch__knob {
position: absolute; top: 3px; left: 3px; z-index: 1;
transition: transform $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__knob {
transform: translateX(44px);
}
04아이콘 전환
노브 안 체크·엑스 아이콘이 opacity로 자리를 바꾸는 사이 노브 자체도 옆으로 이동합니다. 권한 허용·거부처럼 결과가 아이콘 하나로 설명되는 스위치에 씁니다.
.switch__knob { transform: translateX(0); transition: transform $duration $easing; }
.switch__icon--x { opacity: 1; }
.switch__icon--check { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__knob { transform: translateX(26px); }
.switch__input:checked ~ .switch__track .switch__icon--x { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__icon--check { opacity: 1; }
05눌림 스퀴시 효과
누르는 동안 노브가 scaleX로 가로로 퍼졌다가 손을 떼면 원래 원으로 돌아옵니다. 터치 중심 모바일 폼에서 눌렀다는 촉감을 화면으로 대신 전달할 때 씁니다.
.switch__knob {
--x: 0px; --sx: 1; --sy: 1;
transform: translateX(var(--x)) scaleX(var(--sx)) scaleY(var(--sy));
transition: transform $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__knob { --x: 24px; }
.switch:active .switch__knob { --sx: 1.35; --sy: .8; }
06배경색 채움 전환
트랙 배경이 한 번에 바뀌지 않고, 노브가 있던 자리에서 clip-path 원이 점점 커지며 서서히 채웁니다. 프리미엄 플랜 전환처럼 전환 자체를 연출로 보여주고 싶은 카드에 씁니다.
.switch__fill {
background: $color;
clip-path: circle(0% at 18% 50%);
transition: clip-path $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__fill {
clip-path: circle(75% at 82% 50%);
}
07해/달 아이콘 토글
노브가 움직이는 트랙 자체를 하늘로 만들어, 배경 그러데이션이 밝은 낮 하늘색에서 짙은 밤하늘색으로 넘어가고 그 위로 별 세 개가 opacity로 돋아납니다. 테마 전환 스위치를 아이콘 하나보다 크게 보여주고 싶을 때 씁니다.
.switch__track {
background-image: linear-gradient(90deg, $stage-yellow 0%, $stage-yellow 50%, $color 50%, $color 100%);
background-size: 200% 100%;
background-position: 0% 0%;
transition: background-position $duration $easing;
}
.switch__input:checked ~ .switch__track { background-position: 100% 0%; }
.switch__input:checked ~ .switch__track .switch__star { opacity: 1; }
08크기 변형 믹스인
SCSS 믹스인 하나가 트랙 폭·높이·노브 크기·이동 거리를 계산해 작게·보통·크게 세 스위치를 만들고, 셋 다 같은 translateX 애니메이션을 공유합니다. 디자인 시스템 컴포넌트처럼 크기마다 값을 다시 재지 않아도 되는 구조가 필요할 때 씁니다.
@mixin switch-size($track-w, $track-h, $knob) {
--travel: #{$track-w - $knob - 6px};
.switch__track { width: $track-w; height: $track-h; }
.switch__knob { width: $knob; height: $knob; }
}
.switch--sm { @include switch-size(36px, 20px, 14px); }
.switch--md { @include switch-size(48px, 26px, 20px); }
.switch--lg { @include switch-size(60px, 32px, 26px); }
09비활성 상태 스타일
disabled 속성이 붙은 스위치는 filter: grayscale과 opacity 트랜지션으로 흐려지고 커서가 not-allowed로 바뀝니다. 권한이 없는 설정 항목처럼 손댈 수 없다는 사실 자체를 알려야 하는 자리에 씁니다.
.switch__input:disabled ~ .switch__track { filter: grayscale(1); opacity: .45; }
.switch__input:disabled ~ .switch__label { opacity: .55; }
@keyframes fade-loop {
0%, 20% { filter: grayscale(0); opacity: 1; }
50%, 70% { filter: grayscale(1); opacity: .45; }
100% { filter: grayscale(0); opacity: 1; }
}
어디서 깨지는가 — 함정 하나
아홉 개 중 절반 이상이 background-color를 직접 건드리지 않고 두 레이어를 겹쳐 opacity로 교차시킵니다. 색 자체를 애니메이션하면 중간값이 브라우저마다 다르게 보간돼 탁한 회색을 거쳐 가지만, 겹친 레이어를 투명도로만 바꾸면 각 레이어의 원래 색이 그대로 유지됩니다. MDN의 애니메이션 성능 문서가 transform·opacity를 우선하라고 권하는 것도 같은 이유입니다. 06의 clip-path와 07의 background-position은 이 규칙의 예외인데, 둘 다 리페인트만 요구할 뿐 레이아웃을 다시 계산하지 않아 넣었습니다 — caniuse.com에서 clip-path를 찾아보면 주요 브라우저 전부 오래전부터 지원합니다.
함정은 아니지만 짚어둘 점이 하나 있습니다. 03의 ON·OFF 글자는 노브 지름보다 트랙이 넉넉히 넓어야 글자가 절반만 가려지는 어색한 순간이 안 생깁니다. 여기 적힌 트랙 폭과 이동 거리를 그대로 담아 압축한 파일은 a2ejkfw2을 입력해야 열리며, 버튼은 바로 아래에 있습니다.
접근성
아홉 개 모두 <input type="checkbox">에 role="switch"를 얹은 네이티브 컨트롤이라 Tab으로 초점이 오고 Space로 값을 바꿀 수 있습니다(09는 disabled라 애초에 초점 순서에서 빠집니다). :focus-visible에는 아래처럼 링을 그렸고, 01~08은 change 이벤트마다 aria-checked를 JS로 맞춰 오래된 스크린리더까지 상태를 놓치지 않게 했으며, 다시 켜질 일이 없는 09는 aria-checked="false"를 마크업에 고정해 뒀습니다.
<label class="switch">
<input type="checkbox" class="switch__input" role="switch" aria-checked="false">
<span class="switch__track"><span class="switch__knob"></span></span>
</label>
.switch__input:focus-visible ~ .switch__track {
outline: 2px solid currentColor;
outline-offset: 3px;
}
prefers-reduced-motion: reduce가 켜진 화면에서는 아홉 개 전부 전환 애니메이션만 꺼지고 켜짐·꺼짐 상태 자체는 그대로 드러납니다. 09는 원래도 손댈 수 없는 상태라 이 설정과 무관하게 항상 흐려진 모습입니다. 지원 범위는 MDN의 prefers-reduced-motion 문서에서 확인할 수 있습니다.
체크박스형 컨트롤을 더 보고 싶다면 CSS 애니메이션 모음 목록을 훑어보시고, 이 사이트가 어떤 곳인지 궁금하면 운영자 소개 페이지를 먼저 읽어 보십시오.
FAQ
토글 스위치와 체크박스는 뭐가 다른가요?
마크업은 같은 <input type="checkbox">지만 role="switch"를 얹으면 스크린리더가 "체크박스" 대신 "스위치"로 읽어 줍니다. 즉시 반영되는 설정(알림 켜기 등)에는 스위치, 저장 버튼을 눌러야 반영되는 항목에는 체크박스가 더 맞는 신호입니다.
09처럼 disabled인데 왜 움직이나요?
실제 컨트롤은 항상 비활성 상태로 고정돼 있고, 화면에 반복 재생되는 흐려짐은 disabled가 켜질 때의 전환을 보여주려는 데모용 루프일 뿐입니다. 사람이 직접 클릭해 켜고 끄는 동작은 애초에 불가능합니다.
React에서는 클래스 토글 대신 뭘 쓰나요?
zip 안 react/ 폴더의 컴포넌트는 checked를 useState로 들고, 변수 세 개는 props로 받아 CSS 변수로 넘깁니다. SCSS 모듈은 var()만 읽으므로 값 몇 개만 바꿔 다른 색·속도의 스위치를 바로 만들 수 있습니다.