Checkbox
A native checkbox whose tick pops in and draws itself, with an indeterminate dash, three sizes and a spring press. Every animation is CSS.
Installation
Add the registry to components.json, once
{
"registries": {
"@rabtx": "http://localhost:3002/r/{name}.json"
}
}Add the component
npx shadcn@latest add @rabtx/checkboxOr, without the namespace, by URL:
npx shadcn@latest add http://localhost:3002/r/checkbox.jsonThe files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.
Usage
Installed from the registry, import from @/components/ui/checkbox; inside this monorepo, from
@rabtx/ui/checkbox.
import { Checkbox } from "@rabtx/ui/checkbox";
export function Terms() {
const [agreed, setAgreed] = useState(false);
return (
<Checkbox checked={agreed} onCheckedChange={setAgreed}>
I agree to the terms
</Checkbox>
);
}It is a real <input type="checkbox">, so Space toggles it, forms submit it (name, value,
required) and screen readers announce it. With children it wraps the box in a <label>, so the
text names it and the whole row is clickable. A bare checkbox needs an aria-label or a Field.
Description
description adds a quieter line under the label. It is linked with aria-describedby, so it is
read as the description rather than as part of the name.
Indeterminate
indeterminate (or shadcn's checked="indeterminate") draws a dash, for a "select all" above a
list when only some rows are chosen. It always follows your state: the browser would clear it on a
click, so the component sets it back after every click and render. Compute it from your data.
States
Off, on, indeterminate and disabled (half opacity, not clickable). Inside a Field, error turns
the edge red with a soft halo, sets aria-invalid and links the message; aria-invalid on its own
does the same.
Accept the terms to continue.
Controlled
Checked: false
API
Checkbox takes everything a native checkbox takes, except type and size, plus:
Prop
Type
Outside React, the class names are exported from @rabtx/ui/checkbox/styles (checkboxClass(),
checkboxParts, checkboxPaths):
<label class="rx-checkbox-label">
<span class="rx-checkbox rx-checkbox-md">
<input type="checkbox" class="rx-checkbox-input" />
<svg class="rx-checkbox-mark rx-checkbox-check" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 13l4 4L19 7" pathLength="1" />
</svg>
<svg class="rx-checkbox-mark rx-checkbox-dash" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 12h12" pathLength="1" />
</svg>
</span>
<span class="rx-checkbox-text"><span>Email me updates</span></span>
</label>Theming
The Checkbox reads only the theme tokens.
| Part | Tokens |
|---|---|
| Box, off | fill --background, edge --muted-foreground at 50% (full on hover) |
| Box, on and indeterminate | fill and edge --primary, glyph --primary-foreground |
| Focus and error | --ring, --destructive (edge, and a 15% halo) |
| Label and description | --foreground, --muted-foreground |
| Corners | --radius-check, scaled with the box (1×, 1.25×, 1.5×) |
| Motion | --spring-check (draw and pop), --spring-press (press), --motion-ease (colors, exit) |
How the motion works
| Effect | CSS |
|---|---|
| State | the input is invisible and laid over the box; the box reads it with :has(> :checked), :has(> :indeterminate), :has(> :focus-visible), :has(> :disabled) |
| Fill | background-color and border-color transition in 200ms |
| Pop | the glyph's SVG goes from opacity 0, scale: 0.5 and blur(4px) to rest in half the check spring |
| Draw | each path has pathLength="1", stroke-dasharray: 1 and transitions stroke-dashoffset from 1 to 0 on the check spring (a critically damped linear() easing, about 300ms), 40ms after the pop starts; the dash draws a third quicker |
| Exit | the glyph fades, shrinks and blurs out in 160ms; its stroke resets to undrawn after that, unseen |
| Swap | tick and dash are two SVGs, so switching between checked and indeterminate cross-fades them |
| Press | :has(> :active) on the box, or :active on its label, springs it to scale: 0.92 on the press spring |
Accessibility
- A native checkbox: role, checked and mixed states, Space, form submission and labels come from the browser.
- Keyboard focus shows a 2px ring (red when invalid). The click target is at least 24px.
- Disabled is half opacity and does not react or animate.
- With reduced motion there is no press, pop, blur or draw; the glyph fades in 100ms. With
data-motion="off"every state changes at once.