Studio KitInteractive Buttons and Controlsmotion/react

AnimatedCheckbox

Checkbox whose check mark draws itself as an SVG path with a squash-and-settle box pop, plus an optional animated strike-through across the label.

Preview

Customize

5 props

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add animated-checkbox

Writes components/ui/animated-checkbox.tsx and components/styles/animated-checkbox.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { AnimatedCheckbox } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <AnimatedCheckbox />6  );7}

Props

PropTypeDefault
checked

Controlled checked state.

booleanundefined
defaultChecked

Initial state when uncontrolled.

booleanfalse
onCheckedChange

Called with the next state when toggled.

(checked: boolean) => voidundefined
label

Visible label, also the accessible name.

string'Ship the release notes'
strikeThrough

Draw a strike-through line across the label when checked.

booleantrue
accentColor

Box fill color when checked.

string'#38bdf8'
disabled

Disables interaction.

booleanfalse
className

Extra class merged onto the root button.

string''

Accessibility

Renders a <button role="checkbox"> with aria-checked and the label as accessible name (fallback aria-label "Checkbox"); the box, check path and strike line are aria-hidden. Space/Enter toggle natively, and prefers-reduced-motion shows the final state instantly.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/animated-checkbox.tsx
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/animated-checkbox.module.css";6 7export interface AnimatedCheckboxProps {8  /** Controlled checked state */9  checked?: boolean;10  /** Initial state when uncontrolled */11  defaultChecked?: boolean;12  /** Called with the next state when toggled */13  onCheckedChange?: (checked: boolean) => void;14  /** Visible label (also the accessible name) */15  label?: string;16  /** Draw a strike-through line across the label when checked */17  strikeThrough?: boolean;18  /** Box fill color when checked */19  accentColor?: string;20  /** Disable interaction */21  disabled?: boolean;22  /** Optional className */23  className?: string;24}25 26export function AnimatedCheckbox({27  checked,28  defaultChecked = false,29  onCheckedChange,30  label = "Ship the release notes",31  strikeThrough = true,32  accentColor = "#38bdf8",33  disabled = false,34  className = "",35}: AnimatedCheckboxProps) {36  const prefersReducedMotion = useReducedMotion();37  const [inner, setInner] = React.useState(defaultChecked);38  const isControlled = checked !== undefined;39  const on = isControlled ? checked : inner;40 41  const toggle = () => {42    if (disabled) return;43    const next = !on;44    if (!isControlled) setInner(next);45    onCheckedChange?.(next);46  };47 48  const ease = [0.65, 0, 0.35, 1] as const;49 50  return (51    <button52      type="button"53      role="checkbox"54      aria-checked={on}55      aria-label={label ? undefined : "Checkbox"}56      disabled={disabled}57      onClick={toggle}58      data-state={on ? "checked" : "unchecked"}59      style={{ "--mf-ac-accent": accentColor } as React.CSSProperties}60      className={`${styles.root} ${className}`.trim()}61    >62      <motion.span63        aria-hidden="true"64        className={styles.box}65        initial={false}66        animate={prefersReducedMotion ? { scale: 1 } : { scale: on ? [1, 0.82, 1.06, 1] : 1 }}67        transition={{ duration: prefersReducedMotion ? 0 : 0.35 }}68      >69        <svg viewBox="0 0 24 24" className={styles.icon}>70          <motion.path71            d="M5 12.5 L10 17.5 L19 7"72            fill="none"73            stroke="currentColor"74            strokeWidth="3"75            strokeLinecap="round"76            strokeLinejoin="round"77            initial={false}78            animate={{ pathLength: on ? 1 : 0, opacity: on ? 1 : 0 }}79            transition={{80              pathLength: { duration: prefersReducedMotion ? 0 : 0.3, ease, delay: on && !prefersReducedMotion ? 0.06 : 0 },81              opacity: { duration: prefersReducedMotion ? 0 : 0.05, delay: on ? 0 : 0.25 },82            }}83          />84        </svg>85      </motion.span>86      {label ? (87        <span className={styles.label}>88          {label}89          {strikeThrough ? (90            <motion.span91              aria-hidden="true"92              className={styles.strike}93              initial={false}94              animate={{ scaleX: on ? 1 : 0 }}95              transition={{ duration: prefersReducedMotion ? 0 : 0.35, ease, delay: on && !prefersReducedMotion ? 0.15 : 0 }}96            />97          ) : null}98        </span>99      ) : null}100    </button>101  );102}