Starter Docs
UI

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

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/checkbox

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/checkbox.json

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

PartTokens
Box, offfill --background, edge --muted-foreground at 50% (full on hover)
Box, on and indeterminatefill 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

EffectCSS
Statethe input is invisible and laid over the box; the box reads it with :has(> :checked), :has(> :indeterminate), :has(> :focus-visible), :has(> :disabled)
Fillbackground-color and border-color transition in 200ms
Popthe glyph's SVG goes from opacity 0, scale: 0.5 and blur(4px) to rest in half the check spring
Draweach 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
Exitthe glyph fades, shrinks and blurs out in 160ms; its stroke resets to undrawn after that, unseen
Swaptick 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.

On this page