Inputs

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

180
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
checkedboolean | "indeterminate"—Controlled state; managed internally (uncontrolled) when omitted. Pass "indeterminate" for mixed
defaultCheckedboolean | "indeterminate"falseInitial state in uncontrolled mode
onCheckedChange(next: boolean) => void—Called when the state changes; clicking from indeterminate always yields true
indeterminatebooleanfalseConvenience prop: when true shows the mixed state, same as checked="indeterminate"
size"sm" | "md" | "lg""md"Box size: sm 14px, md 16px, lg 20px
durationnumber180Check-mark draw duration (ms); unchecking retracts in half the time
labelReactNode—Label text on the right; clicking the label also toggles
descriptionReactNode—Supporting text below the label
disabledbooleanfalseDisable the checkbox
namestring—Form field name; when set, a hidden input is submitted (only while checked)
valuestring—Submitted value; inside a Group it also identifies the item
selectAllbooleanfalseInside a Group, act as the select-all row: derives checked / mixed and checks or clears everything on click
classNamestring—Passed through to the outermost container

DrawnCheckbox.Group

FieldTypeDefaultDescription
valuestring[]—Controlled list of checked values; managed internally (uncontrolled) when omitted
defaultValuestring[][]Initial checked list in uncontrolled mode
onValueChange(value: string[]) => void—Called when the checked list changes
orientation"vertical" | "horizontal""vertical"Layout direction
staggernumber30Interval (ms) by which items are staggered in DOM order on select-all / clear-all
aria-labelledbystring—id of the element that titles the group
aria-labelstring—Group name when there is no visible heading
childrenReactNode—DrawnCheckbox children with a value, plus an optional select-all row with selectAll
classNamestring—Passed through to the group root element

How it works

  • The check mark is a hand-drawn SVG path that draws from pathLength 0 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 d attribute 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 × stagger ms in DOM order; single toggles are not delayed.
  • The colour transition reads --wb-drawn-checkbox-duration (default 180ms); passing duration overrides 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 / defaultChecked are ignored.

Accessibility

  • The box is a button with role="checkbox" and aria-checked (true / false / mixed); the label is linked via aria-labelledby and the description via aria-describedby
  • Clicking the label also toggles; on the keyboard Space toggles and Enter does not, matching a native checkbox
  • Keyboard focus shows a focus-visible ring; when disabled the button is disabled and cannot be focused or toggled
  • The group root is role="group" and can be named with aria-labelledby or aria-label
  • When the user has reduce motion enabled, there is no drawing, squash or stagger — the check simply fades in over 100ms