Checkbox
betav0.1.0Binary and tri-state selection control built on @radix-ui/react-checkbox. Renders a 16px box (role="checkbox", aria-checked including "mixed", Space to toggle) with an optional integrated label and description, so no separate FormControlLabel wrapper is needed. The box is transparent at rest by design and inherits its container's surface; --bg-input-default appears only on hover and focus. Size is intentionally fixed to the single 16px box the Figma spec defines. Colour is fixed too, with one exception: the `error` tone, which Figma added to the component set as state=Error and which recolours the border, the glyphs, the label and the description — see Checkbox.rfc.md.
Variants
Value states (uncontrolled)
Disabled — colour substitution, never opacity
With integrated label
With label and description
On an elevated surface — the box is transparent at rest and picks up the container
Examples
<Checkbox aria-label="Select row" /><Checkbox label="Enable notifications" /><Checkbox label="Auto-refresh" checked={enabled} onCheckedChange={(v) => setEnabled(v === true)} /><Checkbox label="Select all" checked="indeterminate" onCheckedChange={selectAll} /><Checkbox label="Share usage data" description="You can change this later in settings." /><Checkbox label="Read-only mode" disabled /><Checkbox label="I accept the terms" error description="Required to continue." /><Checkbox aria-label="Manage sensitive configuration" error checked={armed} onCheckedChange={(v) => setArmed(v === true)} /><Checkbox ref={checkboxRef} label="Enable notifications" defaultChecked />Props
| Prop | Type |
|---|---|
checked | "boolean | 'indeterminate'" — Controlled value. Use "indeterminate" for select-all headers |
defaultChecked | boolean | 'indeterminate' — Default value (uncontrolled) |
onCheckedChange | (checked: boolean | 'indeterminate') => void — Fires when the value changes. Optional: a Checkbox with `checked` and no handler is a valid presentational control whose click bubbles to the surrounding row |
label | ReactNode — Integrated label rendered next to the box, natively associated with it |
description | ReactNode — Secondary text below the label, indented to align under it and wired via aria-describedby |
wrapperClassName | string — Additional classes for the wrapper holding label and description (only applies when `label` or `description` is set) |
disabled | boolean — Disabled state (colour substitution, never an opacity tint) |
required | boolean — Marks the control as required in a form |
name | string — Name submitted with the owning form |
value | string — Value submitted with the owning form when checked (defaults to "on", like a native checkbox) |
id | string — Overrides the auto-generated id used for label association |
error | boolean — Error tone (Figma state=Error). Recolours the box border, both glyphs, the label and the description to --color-feedback-error-primary; changes no geometry and adds no background. `disabled` wins over it |
className | string — Additional classes for the box itself |
Accessibility
Radix Root renders a native <button> with role="checkbox" and aria-checked (true / false / "mixed" for indeterminate), Space toggles, and controlled or uncontrolled value works through checked/defaultChecked + onCheckedChange. forwardRef is exposed on the underlying <button> (repo convention for form-field components). The label uses native <label htmlFor>/<button id> association and the description is wired via aria-describedby. Indeterminate is set programmatically only and is not reachable by keyboard toggle, per the WAI-ARIA convention. onCheckedChange is optional and the component never calls stopPropagation, so a Checkbox can be used purely presentationally inside a clickable row — 12 product call sites inside MUI Autocomplete depend on exactly that. TWO DELIBERATE DEVIATIONS FROM FIGMA, both declared in Checkbox.figma.json with their rationale. First, focus uses the repo's standard ring rather than Figma's 1px-offset/2px-ring geometry: one focus indicator across the library, defined and tested once. The ring colour is identical and the indicator is at least as thick, so SC 2.4.11 and SC 2.4.13 are preserved or improved. Second, disabled is expressed by substituting colours rather than by an opacity tint — which is what Figma binds. THE BOX HAS NO BACKGROUND AT REST. This is an arbitrated design decision, not an omission: the box is transparent and inherits its container's surface, so the 1px border is what carries perception when unchecked and unfocused. Contrast pairs are therefore declared against the standard reference surface only. Disabled state is exempt from contrast (WCAG 1.4.3 Note 1, inactive UI components): --bg-input-disabled, --text-default-disabled, --text-input-disabled and --icon-disabled are reached exclusively through disabled: variants. The disabled label pair is declared anyway and clears its threshold at 9.38:1 dark / 7.20:1 light. MEASURED RATIOS, computed by pnpm check:contrast against the standard reference surface, dark then light. Everything that carries the component or its state is above threshold: box border at rest 3.14:1 / 3.72:1 (needs 3:1); check and dash glyph 10.30:1 / 11.22:1; focus ring 10.30:1 / 11.22:1; label text 17.38:1 / 15.72:1; description text 8.99:1 / 7.89:1. Two SUPPLEMENTAL treatments are deliberately not declared as pairs: the hover and focus box fill --bg-input-default at 1.22:1 / 1.13:1, and the hover halo at 1.17:1 / 1.22:1. Neither is required to identify the component or its value, and neither could clear 3:1 without ceasing to read as a hover. The box is identified by its border, which is above threshold in both modes, and the value is carried by the glyph and by aria-checked, both also above threshold. Understanding SC 1.4.11's 'Hover states' guidance is explicit that author-supplied hover treatments can be considered supplemental. Their Rule 5 coverage is signed off in scripts/contrast-coverage-allowlist.json (dated, with a removal condition) — the coverage mechanism, not a WCAG derogation: there is no wcag-allowlist entry for this component. wcagStatus is pass because the component is conformant, not by exemption. The underlying tooling gap — ContrastPair cannot mark a pair supplemental, so a supplemental treatment can only be omitted or counted against the verdict — is tracked HIGH PRIORITY in process/AI-BACKLOG.md. Size, color, disableRipple and edge are excluded — see Checkbox.rfc.md section 7.