Animated Checkbox

Ticking a to-do fills the box with a squish, draws the check mark, and strikes the task through from left to right.

  • CSS
  • Forms & controls
  • Toggle
  • Added

AI prompt

Create an animated to-do checkbox in pure CSS on a native checkbox. Hide the native input visually (keep it focusable) and draw a 22px rounded box next to the label text. When :checked, fill the box with the accent colour and play a 300ms keyframe that squishes it (scale 1 → 0.85 → 1.08 → 1). Draw the check with an SVG path whose stroke-dasharray equals its length; transition stroke-dashoffset from the full length to 0 over 260ms with a 120ms delay. Strike the label text with a ::after line that scales from 0 to 1 on the x axis with transform-origin left over 360ms, and fade the text to the muted colour. Show a focus ring around the box when the input has :focus-visible. Under prefers-reduced-motion, apply the checked state without animation. The tasks read "Sketch the storyboard", "Pick the easing", and "Ship the demo".

Source

Demo.tsx
'use client'

import s from './demo.module.css'

export default function AnimatedCheckbox({ labels }: { labels: Record<string, string> }) {
  return (
    <ul className={s.list}>
      {[labels.a, labels.b, labels.c].map((t, i) => (
        <li key={t}>
          <label className={s.row}>
            <input type="checkbox" className={s.input} defaultChecked={i === 0} />
            <span className={s.box} aria-hidden="true">
              <svg viewBox="0 0 24 24">
                <path d="M6 12.5l4 4 8-9" />
              </svg>
            </span>
            <span className={s.text}>{t}</span>
          </label>
        </li>
      ))}
    </ul>
  )
}
demo.module.css
.list {
  display: grid;
  gap: 14px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  cursor: pointer;
}

.input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.box {
  display: grid;
  place-items: center;
  flex: none;
  width: 22px;
  height: 22px;
  border: 2px solid var(--stage-rule);
  border-radius: 6px;
  transition: background-color 200ms var(--ease-out), border-color 200ms var(--ease-out);
}

.box svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--stage-accent-ink);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
  transition: stroke-dashoffset 260ms var(--ease-out);
}

.text {
  position: relative;
  font: 500 var(--text-base) / 1.3 var(--font-body);
  color: var(--stage-ink);
  transition: color 300ms var(--ease-out);
}

.text::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 52%;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 360ms var(--ease-out);
}

.input:checked + .box {
  border-color: var(--stage-accent);
  background: var(--stage-accent);
  animation: squish 300ms var(--ease-out);
}

.input:checked + .box svg { stroke-dashoffset: 0; transition-delay: 120ms; }
.input:checked ~ .text { color: var(--stage-muted); }
.input:checked ~ .text::after { transform: scaleX(1); }
.input:focus-visible + .box { outline: 2px solid var(--color-focus); outline-offset: 3px; }

@keyframes squish {
  35% { transform: scale(0.85); }
  70% { transform: scale(1.08); }
}

@media (prefers-reduced-motion: reduce) {
  .box, .box svg, .text, .text::after { transition: none; }
  .input:checked + .box { animation: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.