Checkbox
A hand-drawn Base UI checkbox styled with shadcn conventions. The box is a sketched outline and the tick is the Boxicons check, wobbled by a seeded SVG filter so it overshoots the top-right corner like a pen mark.
Installation
npx shadcn@latest add https://sketchcn.chuwii.com/r/checkbox.jsonStyles
Checkbox leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.
[data-sketch-outline] path[fill]:not([fill="none"]) {
fill: var(--sketch-fill);
fill-opacity: var(--sketch-fill-opacity, 1);
transition:
fill 150ms ease,
fill-opacity 150ms ease;
}
@keyframes sketch-check-boil {
0% {
filter: var(--sketch-check-frame-1);
}
33% {
filter: var(--sketch-check-frame-2);
}
66% {
filter: var(--sketch-check-frame-3);
}
100% {
filter: var(--sketch-check-frame-0);
}
}Read the whole file for the drawing variables, paper textures and reduced-motion rules.
Usage
import { Checkbox } from "@/components/ui/checkbox";
export function Terms() {
return <Checkbox aria-label="Accept terms" />;
}Props
Checkbox adds no props of its own. Everything is forwarded to the Base UI Checkbox.Root primitive.
Each checkbox bends its tick with an feTurbulence and feDisplacementMap filter seeded from the SketchProvider seed and the checkbox instance, so every tick wobbles differently. The pick is stable across renders and hydration; change the SketchProvider seed to reshuffle every tick.
| Variable | Default | Description |
|---|---|---|
--sketch-stroke | var(--primary) | Ink colour of the box. |
--sketch-fill-opacity | 0, 0.1 on hover | Wash behind the box. |
--sketch-check-frame-0 … 3 | url(#…) | The four seeded filters the tick boils through. |
The tick takes its colour from the checkbox text-* class, text-primary by default.
Examples
With label
import { Checkbox } from "@/components/ui/checkbox";
export function TermsCheckbox() {
const id = useId();
return (
<div className="flex items-center gap-2">
<Checkbox id={id} />
<label htmlFor={id} className="text-sm">
Accept terms and conditions
</label>
</div>
);
}Todo list
import { Checkbox } from "@/components/ui/checkbox";
const TODOS = ["Sharpen pencils", "Sketch wireframes", "Ink the final lines"];
export function TodoList() {
const id = useId();
const [done, setDone] = useState<string[]>([TODOS[0]]);
const toggleTodo = (todo: string, checked: boolean) => {
setDone((current) => {
if (checked) {
return [...current, todo];
}
return current.filter((item) => item !== todo);
});
};
return (
<div className="flex flex-col gap-3">
{TODOS.map((todo, index) => (
<div key={todo} className="flex items-center gap-2">
<Checkbox
id={`${id}-${index}`}
checked={done.includes(todo)}
onCheckedChange={(checked) => toggleTodo(todo, checked)}
/>
<label htmlFor={`${id}-${index}`} className="text-sm">
{todo}
</label>
</div>
))}
</div>
);
}Animation
Checking draws the tick in from left to right by transitioning the indicator's clip-path from Base UI's data-starting-style. Unchecking fades it out.
While checked, the tick keeps boiling through its four seeded filters with the sketch-check-boil keyframe from sketch.css, like a line redrawn frame by frame. Both motions are skipped under prefers-reduced-motion.
"animate-[sketch-check-boil_1200ms_steps(1)_infinite]"