Drawn Checkbox
A React checkbox whose check mark draws itself with a small squash-and-settle, morphing smoothly between the indeterminate dash and the check, with staggered select-all in groups.
A checkbox whose check mark draws along its stroke when ticked, while the box squashes slightly and springs back the moment its state changes. The indeterminate dash and the check are the same path, so they morph into each other instead of being swapped. Pair it with DrawnCheckbox.Group to build a list with a "select all" row: the row derives checked / mixed on its own, and checking or clearing everything staggers the items in on-screen order. Both the single checkbox and the group support controlled and uncontrolled modes.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add drawn-checkbox.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<DrawnCheckbox/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/drawn-checkbox.en.jsonOr, once registries are configured in components.json, install it as @webberui/drawn-checkbox.
Usage
import { DrawnCheckbox } from "@/components/ui/drawn-checkbox";
// Single checkbox (uncontrolled)
<DrawnCheckbox label="Accept the terms of service" description="Required before you can continue" />
// Group + select-all row
const [done, setDone] = React.useState<string[]>(["reply"]);
<DrawnCheckbox.Group value={done} onValueChange={setDone} aria-label="Today's to-dos">
<DrawnCheckbox selectAll label="Select all" />
<DrawnCheckbox value="reply" label="Reply to client emails" />
<DrawnCheckbox value="report" label="Tidy up the Q3 report" />
<DrawnCheckbox value="dentist" label="Book a dentist appointment" description="Handed off to a teammate" disabled />
</DrawnCheckbox.Group>Props
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | "indeterminate" | — | Controlled state; managed internally (uncontrolled) when omitted. Pass "indeterminate" for mixed |
defaultChecked | boolean | "indeterminate" | false | Initial state in uncontrolled mode |
onCheckedChange | (next: boolean) => void | — | Called when the state changes; clicking from indeterminate always yields true |
indeterminate | boolean | false | Convenience prop: when true shows the mixed state, same as checked="indeterminate" |
size | "sm" | "md" | "lg" | "md" | Box size: sm 14px, md 16px, lg 20px |
duration | number | 180 | Check-mark draw duration (ms); unchecking retracts in half the time |
label | ReactNode | — | Label text on the right; clicking the label also toggles |
description | ReactNode | — | Supporting text below the label |
disabled | boolean | false | Disable the checkbox |
name | string | — | Form field name; when set, a hidden input is submitted (only while checked) |
value | string | — | Submitted value; inside a Group it also identifies the item |
selectAll | boolean | false | Inside a Group, act as the select-all row: derives checked / mixed and checks or clears everything on click |
className | string | — | Passed through to the outermost container |
DrawnCheckbox.Group
| Field | Type | Default | Description |
|---|---|---|---|
value | string[] | — | Controlled list of checked values; managed internally (uncontrolled) when omitted |
defaultValue | string[] | [] | Initial checked list in uncontrolled mode |
onValueChange | (value: string[]) => void | — | Called when the checked list changes |
orientation | "vertical" | "horizontal" | "vertical" | Layout direction |
stagger | number | 30 | Interval (ms) by which items are staggered in DOM order on select-all / clear-all |
aria-labelledby | string | — | id of the element that titles the group |
aria-label | string | — | Group name when there is no visible heading |
children | ReactNode | — | DrawnCheckbox children with a value, plus an optional select-all row with selectAll |
className | string | — | Passed through to the group root element |
How it works
- The check mark is a hand-drawn SVG path that draws from
pathLength0 to 1 (duration =duration, easeOut); unchecking retracts it in half the time. - The check and the indeterminate dash are both three-point paths, and the
dattribute is interpolated number by number, so check ↔ dash is a smooth morph; going straight from unchecked to indeterminate snaps the path into place before drawing. - On every state change the box squashes to 0.9, then springs back (stiffness 500, damping 25).
- The select-all row inside a group only counts enabled items: all checked shows the check, some checked shows the dash. Clicking it only changes enabled items; disabled items keep their state. On select-all or clear-all, the items whose state actually changes are delayed by
i × staggerms in DOM order; single toggles are not delayed. - The colour transition reads
--wb-drawn-checkbox-duration(default 180ms); passingdurationoverrides that variable too, so colour and stroke finish together. - Inside a Group, an item's checked state is driven by the Group's
value;checked/defaultCheckedare ignored.
Accessibility
- The box is a
buttonwithrole="checkbox"andaria-checked(true/false/mixed); the label is linked viaaria-labelledbyand the description viaaria-describedby - Clicking the label also toggles; on the keyboard Space toggles and Enter does not, matching a native checkbox
- Keyboard focus shows a
focus-visiblering; when disabled the button isdisabledand cannot be focused or toggled - The group root is
role="group"and can be named witharia-labelledbyoraria-label - When the user has reduce motion enabled, there is no drawing, squash or stagger — the check simply fades in over 100ms
Select
A button-triggered select whose value crossfades on change, with an anchored list that flips at edges, groups and separators, typeahead and arrow-key navigation, and a sliding highlight.
Radio Group
A React radio group: the selected dot springs into place, and in the card variant the selection outline slides between options, including on arrow-key changes. Supports controlled/uncontrolled use, vertical or horizontal layout, and form submission.