Input·02.8
Animated Checkbox
The tick draws itself, the label strikes through.
Implement Checkbox
Write documentation
Add tests
Install
One dependency. The component is copied into your project, so the file is yours after that.
bun add motionOr let the shadcn CLI do the copying: same file, landing in components/yugo.
bunx shadcn@latest add https://yugo.click/r/animated-checkbox.jsonUsage
"use client";
import { AnimatedCheckbox } from "@/components/yugo/animated-checkbox";
export function TaskList() {
return (
<div className="flex flex-col gap-2">
<AnimatedCheckbox title="Task to complete" />
<AnimatedCheckbox title="Already done" defaultChecked />
<AnimatedCheckbox title="Ship it" onCheckedChange={(checked) => save(checked)} />
</div>
);
}Source
"use client";
import { motion } from "motion/react";
import { useState } from "react";
interface AnimatedCheckboxProps {
title?: string;
defaultChecked?: boolean;
className?: string;
onCheckedChange?: (checked: boolean) => void;
}
const springTransition = {
type: "spring" as const,
duration: 0.4,
bounce: 0.2,
};
export function AnimatedCheckbox({
title = "Implement Checkbox",
defaultChecked = false,
className = "",
onCheckedChange,
}: AnimatedCheckboxProps) {
const [checked, setChecked] = useState(defaultChecked);
const handleClick = () => {
const newChecked = !checked;
setChecked(newChecked);
onCheckedChange?.(newChecked);
};
return (
<div
className={`flex cursor-pointer select-none items-center gap-3 ${className}`}
onClick={handleClick}
>
<div
className={`size-4.5 flex items-center justify-center rounded-[6px] border-[1.5px] transition-colors duration-200 ${
checked
? "border-transparent bg-stone-900 dark:bg-stone-100"
: "border-stone-400/40 bg-transparent hover:border-stone-400/60 dark:border-stone-500/40 dark:hover:border-stone-500/60"
}`}
>
<svg
viewBox="0 0 20 20"
className="size-full text-white dark:text-stone-900"
>
<motion.path
d="M 0 4.5 L 3.182 8 L 10 0"
fill="transparent"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
transform="translate(5 6)"
initial={{
pathLength: defaultChecked ? 1 : 0,
opacity: defaultChecked ? 1 : 0,
}}
animate={{
pathLength: checked ? 1 : 0,
opacity: checked ? 1 : 0,
}}
transition={{
pathLength: { ease: "easeOut", duration: 0.3 },
opacity: { duration: 0 },
}}
/>
</svg>
</div>
<div className="relative">
<span
className={`text-base font-medium transition-colors duration-200 ${
checked
? "text-stone-400 dark:text-stone-500"
: "text-stone-800 dark:text-stone-100"
}`}
>
{title}
</span>
<motion.div
className="absolute left-0 top-1/2 h-[1.5px] -translate-y-1/2 bg-stone-400 dark:bg-stone-500"
initial={{
width: defaultChecked ? "100%" : 0,
opacity: defaultChecked ? 1 : 0,
}}
animate={{
width: checked ? "100%" : 0,
opacity: checked ? 1 : 0,
}}
transition={springTransition}
/>
</div>
</div>
);
}Props
title"Implement Checkbox"stringThe label text for the checkbox.
defaultCheckedfalsebooleanInitial checked state. It renders already ticked rather than animating on mount.
onCheckedChange(checked: boolean) => voidCallback when checked state changes.
className""stringAdditional CSS classes, appended to the row.