GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Checkbox Radio Animations — Copy-Paste

A CSS checkbox radio keeps the browser's real input in place and lets :checked alone redraw whatever sits next to it, one line, copy-paste onto a form that

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below tracks how many boxes a click in a custom radio checkbox css set actually touches. 01, 02, 06, and 09 each toggle one independent checkbox on and off. 03, 05, 07, and 08 are radios, where picking one always clears whichever option lit up before it. 04 sits between the two: it's wired as a checkbox, but its knob-and-track shape borrows the look everyone reads as a switch. If that borrowed knob-and-track shape is all you came for, nine CSS toggle switches cover it properly, and if item 08's stars are closer to the mark, the CSS heart animations handle likes and ratings as their whole subject.

01Checkmark stroke draw

How it works

Checking the box drains an SVG path's stroke-dashoffset from 20 down to 0, so the checkmark looks hand-drawn from its bottom-left point up to its tip rather than just appearing.

Where it fits

A terms-agreement checkbox or a single to-do item both benefit from just over half a second of "I saw this happen."

Checkmark stroke draw
Open in a new tab

01 · Checkmark stroke draw — Measured area 0.683% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

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; }

02Bouncy check

How the css checkbox animation bounces

The instant the box is checked, its icon scales from 0 up to 1.3 and settles back at 1 through a single @keyframes bounce, instead of the plain stroke draw item 01 uses.

Where it fits

Multi-select settings rows and an add-to-cart checkbox both want that extra snap since the click is the whole point of the interaction.

Bouncy check
Open in a new tab

02 · Bouncy check — Measured area 0.583% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

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; }
}

03Radio fill animation

How it works

Three radios share one name, so picking a second option flips its sibling's :checked off in the same instant. The same rule that grows the new dot from scale(0) to scale(1) shrinks the old one back down.

Where it fits

Payment-method pickers and shipping-option lists rely on exactly one dot ever being visible.

Radio fill animation
Open in a new tab

03 · Radio fill animation — Measured area 0.258% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

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); }

04Switch-style checkbox

How it works

This is a checkbox pretending to be a switch: the knob never moves, and a clip-path: circle() layer underneath it grows from a 0% dot to a 75% circle to fill the track with color.

Where it fits

Two small icons inside the knob cross-fade through opacity and scale at the same time, so a moon or sun swaps in without the knob sliding an inch.

Switch-style checkbox
Open in a new tab

04 · Switch-style checkbox — Measured area 0.803% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

05Card-select style

How it works

:has(.sr-input:checked) reaches up from a buried radio to style the whole .card wrapper around it, lighting the border and popping a check badge into view with no shared class or JavaScript in between.

Where it fits

Pricing-tier cards and shipping-method cards both need the entire card to read as selected, not just a small dot in the corner.

Card-select style
Open in a new tab

05 · Card-select style — Measured area 10.862% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

: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);
}

06Todo strikethrough

How it works

Checking a to-do draws a scaleX(0) to scaleX(1) line across the label from an ::after pseudo-element while the text itself fades to 45% opacity. The strike and the fade run on the same clock instead of one snapping ahead of the other.

Where it fits

Any checklist or task list uses the pairing to mark an item done without deleting the row outright.

Todo strikethrough
Open in a new tab

06 · Todo strikethrough — Measured area 0.999% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

scaleXopacity0.3초
.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 .strike { transform: scaleX(1); }
.sr-input:checked ~ .todo__text .label { opacity: .45; }

07Color swatch pick

How it works

Each swatch is its own label, and :has(.sr-input:checked) grows a ring around whichever one holds the checked radio, from scale(0) to scale(1) with a matching opacity fade.

Where it fits

Product color pickers and theme selectors both skip adding a border to every swatch up front, since the ring only exists once something is actually chosen.

Color swatch pick
Open in a new tab

07 · Color swatch pick — Measured area 1.397% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

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; }

08Star-rate input

How it works

The five radios sit in the markup 5 down to 1, and flex-direction: row-reverse flips that back to reading 1 through 5 on screen. That's the trick that lets one sibling selector light every star up to and including the checked one, not just the star itself.

Where it fits

Review forms and satisfaction surveys use the same five inputs a <select> would need, minus the dropdown.

Star-rate input
Open in a new tab

08 · Star-rate input — Measured area 0.636% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

~ 형제 선택자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;
}

09Chip toggle

How it works

Pressing a chip fills its background from the center out via scaleX(0) to scaleX(1), and a separate :active rule scales the whole chip to 0.96 for the length of the press. The fill and the squeeze never fight over the same transform.

Where it fits

Tag pickers and filter bars lean on it since several chips can sit checked at once.

Chip toggle
Open in a new tab

09 · Chip toggle — Measured area 4.197% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

Where it breaks — hiding the input isn't the same as removing it

Every one of the nine keeps a genuine <input type="checkbox"> or <input type="radio"> in the markup, hidden with position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0); rather than display: none or visibility: hidden.

Hiding versus removing

Swap in either of those two and the input drops out of the tab order entirely. A mouse still clicks the drawn shape next to it, but a keyboard user can no longer reach the control at all, since there's nothing left to focus.

The :has() floor

Items 05 and 07 lean on :has() to style a parent from a checked radio buried inside it, and that selector only reaches back to Chrome and Edge 105, Safari 15.4, and Firefox 121. A browser below those versions still lets every card and swatch get clicked and checked, but it just leaves the border and the badge exactly as they were, so the choice registers without ever looking chosen.

Why the stars are reversed

Item 08 takes the input-hiding trick one step further and reorders the radios themselves — 5, 4, 3, 2, 1 in the markup, flipped visually by row-reverse. That reorder exists purely so one ~ sibling selector can light every star up to the pick. Put the radios back in reading order, and that same selector lights the wrong half of the row.

Item 05 marks a chosen card with a border, but the other common answer is to press the card into its own background with two shadows — the neumorphism design kit builds whole sections out of exactly that pair.

What's in the zip

The password below opens a zip where every demo's autoplay script — the four to six lines each index.html uses to flip checked on a loop — has already been stripped back out. What's left is the click-driven version only: 8zg24hjv, typed exactly as it reads here.

Accessibility

What a screen reader hears

Because the real input stays in the document rather than getting deleted, a screen reader announces "checkbox, checked" or "radio button, 2 of 3" the same way it would for an unstyled form. Tab, Space, and the arrow keys move and flip the value without any extra script. All nine carry their own :focus-visible ring on the shape sitting next to the input — item 06's ring sits on its checkbox box, not on the strikethrough itself. Item 08 is also the one built differently under the hood — its label sits right after its own input in the markup. That's why the rule reaches it with + rather than the ~ every other item uses:

.sr-input:focus-visible + .star .star__icon {
  box-shadow: 0 0 0 3px rgba($subject-blue, .35);
}

Reduced motion

prefers-reduced-motion: reduce drops every transition on this page to a 1ms duration, so a checked box or a picked radio lands on its final look immediately instead of animating there. Item 02 is the one exception — its bounce runs on a @keyframes animation rather than a transition. Reduced motion switches the animation off outright and jumps straight to the checked end state instead of just speeding it up. Either way, nothing about the checked state itself changes, only how fast — or whether — it arrives:

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

MDN's guide to :focus-visible covers when browsers draw that ring instead of the default outline. The rest of this site's form and button patterns sit in the CSS category, and what's free to read here versus what's zipped up is on the about page.

FAQ

Should I use a checkbox or a css radio button style for a custom control like this?

Reach for a checkbox when one option flips on and off by itself, and a radio when picking one option has to clear whichever one was picked before. Item 04 is wired as a checkbox even though it looks like a switch, so several of them can sit on at once — a real radio group could never do that. When nothing has to stay on afterward and only the instant of the press matters, the CSS button click feedback effects are the nine to copy instead.

Do I need a <label>, or is aria-label enough?

aria-label alone covers a swatch or a star where the shape already says what it means. But anything carrying its own sentence — item 01's terms line, for instance — reads better wrapped in a <label>, since that pulls the whole sentence into the clickable area instead of only the small box.

The :checked selector doesn't seem to fire in my React version — why not?

The compiled CSS Module class still ships a working :checked rule. What's missing is usually the checked prop itself, since React needs useState and onChange wired up before the input becomes a controlled one at all. The react/ folder in the zip has all nine built that way already, ready to copy the pattern from.

Enter the archive password

The password is inside this article. You will find it as you read.