GODRICH

CSS 체크박스 커스텀 라디오 애니메이션 9종 — 한 줄 복붙으로

CSS 체크박스 커스텀은 브라우저 기본 네모난 체크박스·동그란 라디오의 <input> 은 지우지 않고 그대로 둔 채, 옆에 놓은 도형만 :checked 하나로 다시 그리는 방법입니다. 아홉 개를 모았고, 그중 하나는 체크박스인데 스위치 모양을 빌린 예외입니다.

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

아홉 개는 인기 순이 아니라 선택지 개수가 늘어나는 순서로 놓았습니다. 01·02·06·09는 체크박스 하나가 혼자 켜졌다 꺼지는 경우이고, 03·05·07·08은 라디오로 묶여 여러 개 중 하나만 남는 경우이며, 04는 체크박스인데 스위치의 생김새만 빌린 예외입니다.

01체크 스트로크 그리기

체크박스를 클릭하면 숨겨둔 <input>:checked 가 SVG 체크 아이콘의 stroke-dashoffset 을 20에서 0으로 줄여, 체크 표시가 왼쪽 아래부터 오른쪽 위까지 그어지듯 나타납니다. 이용약관 동의처럼 "읽고 눌렀다"는 확신을 주고 싶은 자리에 씁니다.

stroke-dashoffset:checked0.6초
.box {
  border: 2px solid rgba($subject-cream, .55);
  transition: background $duration $easing, border-color $duration $easing;
}
.sr-input:checked ~ .box { background: $color; border-color: $color; }

.box__path {
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
  transition: stroke-dashoffset $duration $easing;
}
.sr-input:checked ~ .box .box__path { stroke-dashoffset: 0; }

02바운스 체크

체크되는 순간 원형 박스 안의 체크 아이콘이 scale(0)에서 1.3배로 튀어 올랐다가 1배로 가라앉는 @keyframes 바운스로 등장합니다. 장바구니 담기처럼 눌렀다는 반응이 경쾌해야 하는 곳에 맞습니다.

scale 바운스cubic-bezier0.6초
.sr-input:checked ~ .box .box__svg {
  animation: check-pop $duration $easing forwards;
}

@keyframes check-pop {
  0%   { transform: scale(0); opacity: 0; }
  55%  { transform: scale(1.3); opacity: 1; }
  75%  { transform: scale(.9); }
  100% { transform: scale(1); opacity: 1; }
}

03라디오 채움 애니메이션

라디오 세 개 중 하나를 고르면 테두리 안쪽 점이 scale(0)에서 scale(1)로 커지고, 다른 옵션을 고르는 순간 이전 점은 같은 :checked 규칙에 의해 즉시 사라집니다. 결제 수단이나 배송 옵션처럼 하나만 남는 자리에 씁니다.

scale:checked0.22초
.radio__dot::after {
  content: "";
  width: 12px; height: 12px;
  border-radius: 50%;
  background: $color;
  transform: scale(0);
  transition: transform $duration $easing;
}
.sr-input:checked ~ .radio__dot::after { transform: scale(1); }

04스위치형 체크박스

겉모습은 스위치지만 노브는 가운데에 고정해 두고, 트랙 배경이 clip-path: circle() 로 중심에서 바깥까지 번지며 켜진 상태를 알립니다. 노브 안 두 아이콘은 opacity·scale 로 자리를 바꿔가며 크로스페이드합니다.

clip-path:circle():checked0.3초
.switch__fill {
  position: absolute; inset: 0;
  background: $color;
  clip-path: circle(0% at 50% 50%);
  transition: clip-path $duration $easing;
}
.sr-input:checked ~ .switch__track .switch__fill {
  clip-path: circle(75% at 50% 50%);
}

05카드 선택형

라디오로 묶은 카드 중 하나를 고르면 :has(.sr-input:checked) 로 그 카드의 테두리·배경이 강조되고, 오른쪽 위 배지 안 체크 아이콘이 scale 로 나타납니다. :has() 덕분에 라디오와 배지가 형제가 아니어도 카드 전체를 스타일링할 수 있습니다.

:has()scale0.3초
.card:has(.sr-input:checked) {
  border-color: $color;
  background: rgba($subject-cream, .14);
}
.card:has(.sr-input:checked) .card__badge {
  border-color: $color;
  background: $color;
}
.card:has(.sr-input:checked) .card__badge svg {
  opacity: 1; transform: scale(1);
}

06할일 취소선 애니메이션

체크박스를 켜면 라벨 글자 위로 ::after 취소선이 scaleX(0)에서 scaleX(1)로 왼쪽부터 그어지고, 글자 opacity 가 0.45 로 낮아집니다. 할 일 목록이나 체크리스트의 완료 표시에 씁니다.

scaleXopacity0.3초
.todo__strike {
  position: absolute; inset-inline: 0; top: 50%;
  height: 2px; background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform $duration $easing;
}
.sr-input:checked ~ .todo__text .todo__strike { transform: scaleX(1); }
.sr-input:checked ~ .todo__text .todo__label { opacity: .45; }

07색상 스와치 선택

원형 스와치 중 하나를 고르면 :has() 로 그 label::after 링이 scale(0)에서 scale(1)로 커집니다. 스와치 배경색은 새 hex 를 더하지 않고 디자인 토큰 네 개를 그대로 재사용했습니다.

box-shadow 링scale0.22초
.swatch::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 2px solid $color;
  transform: scale(0);
  opacity: 0;
  transition: transform $duration $easing, opacity $duration $easing;
}
.swatch:has(.sr-input:checked)::after { transform: scale(1); opacity: 1; }

08별점 입력형

라디오 다섯 개를 5→1 역순으로 두고 labelrow-reverse 로 뒤집어 화면엔 1→5 로 보이게 한 뒤, :checked ~ .star .star__icon 형제 선택자 하나로 고른 별과 그 앞의 별을 한 번에 채웁니다. 리뷰나 만족도 조사의 별점 입력에 씁니다.

~ 형제 선택자clip-path0.22초
.stars { display: flex; flex-direction: row-reverse; gap: 6px; }

.sr-input:checked ~ .star .star__icon,
.sr-input:checked ~ .star ~ .star .star__icon {
  background: $color;
}

09칩 토글

칩 모양 체크박스를 누르면 배경이 가운데서부터 scaleX(0)에서 scaleX(1)로 채워지고, :active 상태에서 칩 전체가 scale(.96)으로 살짝 눌립니다. 태그나 필터를 여러 개 동시에 고를 때 씁니다.

scaleXpress scale0.22초
.chip__fill {
  position: absolute; inset: 0;
  background: $color; z-index: 0;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform $duration $easing;
}
.sr-input:checked ~ .chip__fill { transform: scaleX(1); }
.chip:active { transform: scale(.96); }

어디서 깨지는가 — 시각적으로 숨긴 input 이 진짜 함정입니다

아홉 개 전부 진짜 <input type="checkbox"> 또는 <input type="radio">position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0); 로 시각적으로만 숨기고, display: none 이나 visibility: hidden 은 쓰지 않았습니다. 이 둘은 요소를 포커스 순서에서 완전히 빼버려서 키보드만 쓰는 사람은 체크박스 자체를 Tab 으로 만날 수 없게 됩니다. 05 카드 선택형과 07 색상 스와치 선택은 :has() 로 부모 요소를 스타일링하는데, 이 선택자는 Chrome·Edge 105+, Safari 15.4+, Firefox 121+ 에서만 동작하고 그 아래 버전에서는 카드 테두리가 그대로 남아 선택 여부만 덜 보일 뿐 클릭 자체는 멀쩡히 작동합니다.

데모에서 저절로 켜졌다 꺼지는 움직임은 각 index.html 안의 4~6줄짜리 자바스크립트가 checked 값을 주기적으로 뒤집는 것뿐이고, 실제 페이지에 붙일 때는 이 스크립트를 지우고 사람의 클릭이 :checked 를 바꾸게 두면 됩니다. 아홉 개 파일이 그대로 담긴 zip 의 압축 비밀번호는 8zg24hjv 이고, 그 파일 안의 index.html 에는 이 자동 재생 스크립트가 빠져 있습니다.

접근성

아홉 개 모두 실제 <input> 을 숨기되 지우지 않았기 때문에 스크린 리더는 체크박스나 라디오를 있는 그대로 읽고, Tab 과 Space·화살표 키만으로 상태를 바꿀 수 있습니다. prefers-reduced-motion: reduce 에서는 트랜지션 지속시간을 1ms 로 줄이거나(02는 바운스 애니메이션 자체를 꺼서) 움직임은 없애되 체크·선택 상태 자체는 그대로 남깁니다. 포커스가 입력에 가면 MDN :focus-visible 로 옆 도형에 링이 뜨도록 아홉 개 전부에 넣었습니다.

.sr-input:focus-visible ~ .box {
  box-shadow: 0 0 0 3px rgba($subject-cream, .5);
}

@media (prefers-reduced-motion: reduce) {
  .box, .box__path { transition-duration: 1ms !important; }
}

체크박스·라디오 말고 다른 폼 부품 이야기는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

체크박스와 라디오 중 뭘 써야 하나요?

하나만 켜고 끄는 독립된 상태라면 체크박스, 여러 옵션 중 정확히 하나만 남아야 한다면 라디오입니다. 04 스위치형 체크박스는 겉모습만 스위치를 빌렸을 뿐 실제로는 체크박스라서 여러 개를 각자 켜고 끌 수 있습니다.

label 없이 aria-label 만 붙여도 되나요?

짧은 스와치나 별점처럼 시각적으로 의미가 다 드러나는 경우엔 aria-label 로 충분하지만, 01 체크박스처럼 문장이 붙는 항목은 <label> 로 감싸 텍스트까지 클릭 영역에 포함시키는 쪽이 터치 사용성이 좋습니다.

React 판에서는 :checked 선택자가 안 통하는데 어떻게 하나요?

CSS Module 로 컴파일된 클래스에서 :checked 선택자 자체는 그대로 살아 있지만, React 컴포넌트는 checked 값을 useState 로 관리하고 onChange 로 값을 바꿔야 제어 컴포넌트가 됩니다. 이 zip 의 react/ 폴더 아홉 개가 그 패턴을 그대로 보여줍니다.

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

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