Designer view

Checkbox

A binary form control with three visual states — unchecked, checked, and mixed (indeterminate). Used for multi-select lists ("select all that apply"), boolean form fields ("subscribe to newsletter"), and parent-child group toggles where the parent reflects the aggregate state of its children. Distinct from Switch (momentary on/off) and Radio (mutually exclusive within a group).

Also called Tickbox Check box Tri-state checkbox

When to use

Use

For multi-selection from a list of options ("Select all interests that apply"), for boolean form fields ("I agree to the terms"), and for parent-child group toggles ("Select all rows" with mixed state when partial). Pair with a fieldset plus legend when grouping related checkboxes. Use the indeterminate state only when the checkbox represents the aggregate of a controlled child set — never as a styled "default" visual.

Avoid

For mutually exclusive choices — that is `Radio` (when authored) or `SegmentedControl`. For momentary on/off without form submission — that is `Switch` (when authored), which carries `role="switch"` and conveys "this control immediately enables/disables a behaviour" rather than "this is a boolean field". For single-option opt-in confirmations where a `<button>` is more appropriate — checkboxes communicate "field with a value", not "action".

Versus related

  • text-input

    `TextInput` is a free-text field whose value is a string; `Checkbox` is a binary field whose submitted value is the `value` attribute (default `"on"`) only when checked. Both participate in native form submission; both pair with a `<label>` and optional description / error-message slots. The decision test: is the value a string the user types (TextInput) or a boolean field they toggle (Checkbox)?

  • combobox

    `Combobox` lets a user pick one or more options from a listbox bound to a textual filter; `Checkbox` is a single binary field that does not require a popup. Multi-select forms with bounded option counts use a list of Checkbox; large unbounded sets use Combobox with multi-select. The decision test: does the option set fit comfortably as a static list (Checkbox) or does it need filter and overflow handling (Combobox)?

  • radio-group

    `RadioGroup` enforces single-selection mutex via shared `name`; `Checkbox` allows independent multi-selection (or stands alone as a single boolean). RadioGroup produces one value per group; a Checkbox list produces zero-to-N values (one per checked instance). Checkbox uniquely supports the tri-state mixed value for parent/child propagation; RadioGroup has no equivalent. Decision test: can the user select more than one option simultaneously (Checkbox) or exactly one (RadioGroup)?

  • switch

    `Switch` is an immediate-effect on/off control whose activation applies the behaviour right away; `Checkbox` is a boolean form field whose value submits with the form. Checkbox supports the tri-state mixed value for parent/child propagation; Switch is strictly binary. Decision test: does the activation submit a form value (Checkbox) or immediately apply a setting (Switch)?

Checkbox is the canonical binary-form-control primitive — a square input that toggles between unchecked, checked, and the tri-state mixed (indeterminate) value when it represents the aggregate of a child group. Native HTML `<input type="checkbox">` is the canonical wire (only checked checkboxes participate in form submission; the indeterminate visual is JS-only, never an attribute). Five canonical slots — root wrapper, input, indicator glyph, label, optional description and error message — three data states, and a parent/child mixed-state propagation contract that codifies the "select all" pattern. The reference documents the label-association rules, the WCAG hit-target threshold, and the divergence from Switch and Radio.

Highlight
Fig 1.1 · Checkbox · Designer view

Implementations

How specific libraries realise the canonical anatomy. Each entry records the deltas between the canon and the library's surface.

radix Checkbox
import * as Checkbox from '@radix-ui/react-checkbox';
import { CheckIcon, MinusIcon } from '@radix-ui/react-icons';
<form>
<label htmlFor="terms" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Checkbox.Root id="terms" name="terms" required>
<Checkbox.Indicator>
{/* checked renders CheckIcon, indeterminate renders MinusIcon */}
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Root>
Accept terms and conditions
</label>
</form>
{/* Indeterminate / tri-state parent */}
<Checkbox.Root
checked={allChecked ? true : someChecked ? 'indeterminate' : false}
onCheckedChange={(checked) => setAll(checked === true)}
>
<Checkbox.Indicator>
{checked === 'indeterminate' ? <MinusIcon /> : <CheckIcon />}
</Checkbox.Indicator>
</Checkbox.Root>

Divergence

From Type → To Rationale
anatomy[root] reshaped Checkbox.Root renders as a <button role="checkbox"> (not <label> or <div>) The canonical root is a <label> (wrapping the input) or a <div> paired with <label for>, so the label-association is built into the root element itself. Radix.Root renders as a <button role="checkbox"> — a custom interactive element that carries the checked state and ARIA role directly, bypassing the native <label>-wrap approach entirely. Label association is the consumer's responsibility: wrap Root in a <label> or pair it with a <label htmlFor> targeting the Root's id. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
anatomy[input] reshaped <button role="checkbox"> (CheckboxTrigger) + hidden CheckboxBubbleInput for form participation The canonical input slot is a visible native <input type="checkbox"> that carries the interactive, form-participation, and aria-checked roles in one element. Radix splits these concerns: the visible, interactive element is a <button role="checkbox"> (CheckboxTrigger) that holds aria-checked and handles Space-key; a separate hidden CheckboxBubbleInput (<input type="checkbox">) is absolutely positioned off-screen (translateX(-100%)) solely for native form participation and constraint-validation bubbling. The hidden input is rendered only when isFormControl is true (i.e., the Root is inside a <form>). Consumers cannot target the hidden input directly — it is internal. Source: https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/src/checkbox.tsx
anatomy[indicator] extended + forceMount prop (boolean) — mounts Indicator even when unchecked, enabling CSS exit animations Canonical Indicator is decorative-only with no lifecycle control. Radix.Indicator wraps its children in a Presence component and exposes forceMount: when true, the Indicator node stays in the DOM in all states so consumers can animate the exit stroke. This is a pure extension — the indicator is still decorative and aria-hidden in all states. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
anatomy[label] omitted Radix ships no Label sub-component. The canonical label slot — a <label> or <label for> that provides the accessible name — is entirely the consumer's responsibility. Consumers compose a native <label> wrapping Checkbox.Root (click on label activates the button) or a <label htmlFor={id}> paired with an id on Root. Without this, the button's accessible name is empty and SR users hear "checkbox" with no field meaning. Radix's docs show the consumer-composed label pattern but do not package it. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
anatomy[description] omitted Radix has no description sub-component. Consumers who need auxiliary helper text programmatically linked to the checkbox must author a <span id="desc"> and pass aria-describedby={id} to Checkbox.Root themselves. Radix Root does accept arbitrary HTML attributes including aria-describedby, but the slot is not first-class. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
anatomy[error-message] omitted Radix has no error-message sub-component and no invalid / error state surfaced as a data attribute. Validation error presentation is entirely consumer-composed: add aria-invalid and aria-describedby to Checkbox.Root, render an error span with aria-live="polite" as a sibling. Radix's constraint-validation propagation (via the hidden bubble input) handles native form validity; visual error messaging is out of scope for the primitive. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
axes.properties[size] omitted Radix Checkbox has no size prop. The visual size of the button and indicator are controlled entirely via consumer CSS on Checkbox.Root and Checkbox.Indicator. The canonical sm | md | lg scale is a design-system convention layered above the primitive. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
axes.properties[invalid] omitted Radix ships no invalid prop and exposes no [data-invalid] attribute. Consumers signal invalidity by passing aria-invalid directly to Checkbox.Root (accepted as a passthrough HTML attribute). Native constraint-validation errors are propagated through the hidden bubble input without surfacing a first-class invalid state on the Root. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
axes.states.data[invalid] omitted No [data-invalid] or equivalent data attribute is emitted by Radix. Only [data-state] ("checked" | "unchecked" | "indeterminate") and [data-disabled] are emitted. Styling invalid state requires consumer selectors on aria-invalid="true" rather than a data attribute. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
events[checkedChange] renamed onCheckedChange (Checkbox.Root prop, signature (checked: boolean | 'indeterminate') => void) One-to-one conceptual match. Radix uses the React convention of on-prefixed callback props rather than the canonical camelCase event name. The payload is the new checked value directly (boolean | 'indeterminate'), not an object — the canonical { checked, previousChecked } shape is not reproduced. Consumers needing previousChecked must track it in local state. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
events[validityChange] omitted Radix exposes no validityChange event or equivalent. Native constraint-validation fires through the hidden bubble input's own change/invalid events but these are internal. Consumers who need validation-change notifications must subscribe to the form's native invalid event or manage validity in their own state layer. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
motion.durations reshaped [data-state]="checked" | "unchecked" | "indeterminate" CSS attribute selectors on Checkbox.Root and Checkbox.Indicator; Indicator forceMount enables exit animation Radix ships no motion duration tokens. State changes are communicated via the data-state attribute on both Root and Indicator; consumers write CSS transitions or animations keyed to these selectors using their own duration values. Exit animations require forceMount on Indicator so the node persists in the DOM during the closing phase. The canonical indicatorDraw / stateChange durations are achievable but values are fully consumer-side. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
motion.reducedMotionFallback omitted Radix implements no reduced-motion behaviour. The data-state attribute still flips immediately; suppressing animation under prefers-reduced-motion is entirely the consumer's CSS responsibility. The canonical "instant" fallback is achievable via @media (prefers-reduced-motion: reduce) but is not bundled. Source: https://www.radix-ui.com/primitives/docs/components/checkbox
Why this audit reads the way it does

Radix Checkbox is a minimal, unstyled primitive that covers the accessibility contract (button role="checkbox", aria-checked including "mixed", Space-key toggle, hidden input for form participation) but deliberately omits every design-system slot above the interactive core: no label, description, error-message, size, or invalid prop. The most architecturally significant divergence is Root's element choice: a <button role="checkbox"> rather than a wrapping <label> or native <input type="checkbox">. This shifts label-association responsibility to the consumer and means the visible control is not the form field — the hidden bubble input is. Consumers must be aware of this split to wire aria-describedby, aria-invalid, and label htmlFor correctly. All remaining divergences follow the same Radix philosophy: expose the ARIA state machine and form integration at the lowest layer; leave every design-system concern (sizing, validation UI, description, motion timing) to the consumer layer above the primitive.

react-aria Checkbox
import {
Checkbox,
CheckboxGroup,
Label,
Text,
FieldError,
} from 'react-aria-components';
// Standalone checkbox (controlled)
<Checkbox isSelected={agreed} onChange={setAgreed}>
I accept the terms
</Checkbox>
// With indeterminate (presentational-only — persists regardless of user interaction)
<Checkbox isIndeterminate>Select all</Checkbox>
// CheckboxGroup with description and validation
<CheckboxGroup
value={selected}
onChange={setSelected}
isRequired
>
<Label>Interests</Label>
<Checkbox value="coding">Coding</Checkbox>
<Checkbox value="music">Music</Checkbox>
<Checkbox value="design">Design</Checkbox>
<Text slot="description">Choose at least one interest.</Text>
<FieldError />
</CheckboxGroup>
// isDisabled (renders aria-disabled="true", keeps focusable)
<Checkbox isDisabled>Unavailable</Checkbox>

Divergence

From Type → To Rationale
anatomy[root] reshaped Checkbox (renders a <label> wrapping all children) The canonical root is either a `<label>` wrapping the input or a `<div>` paired with `<label for>`. React Aria's `Checkbox` component renders a single `<label>` element as its root, which wraps both the hidden native input and the consumer-composed children (indicator SVG + label text). The association contract is identical, but the root is always a `<label>`, never the `<div>` + `<label for>` form. There is no separate named root slot — the component itself is the root. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
anatomy[input] reshaped VisuallyHidden native <input type="checkbox"> (internal, not exposed as a slot) React Aria renders a native `<input type="checkbox">` hidden via `VisuallyHidden` for form participation and AT wiring. The interactive surface (hit-target, focus, press events) is managed on the `<label>` root via `useCheckbox` / `usePress` hooks. Consumers cannot reference or style the internal input directly — it is not a named slot. The canonical input slot's accessibility contract (role, aria-checked, form participation) is fully honoured internally; the difference is that the slot is opaque. Source: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/Checkbox.tsx (fetched 2026-05-05)
anatomy[indicator] reshaped consumer-composed SVG children inside Checkbox (no named slot) React Aria provides no named indicator slot. The checkmark and indeterminate dash are composed by the consumer as SVG children inside `Checkbox`, using render-prop state (`isSelected`, `isIndeterminate`) or `data-selected` / `data-indeterminate` CSS attribute selectors to switch glyphs. The docs ship a starter-kit `<div className="indicator">` wrapping conditional SVG polylines as the canonical example, but this is purely consumer code, not a library-exported slot boundary. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
anatomy[label] reshaped children prop (text or JSX nodes) React Aria `Checkbox` has no named `label` slot. The accessible name comes from the `children` prop — text content or JSX passed as children. When `children` is a function (render-props pattern), it receives `CheckboxRenderProps` including `isSelected`, `isIndeterminate`, `isDisabled`, etc. There is no `<Label>` sub-component used inside a standalone `Checkbox`; `Label` is imported for use inside `CheckboxGroup`. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
anatomy[description] reshaped <Text slot="description"> inside CheckboxGroup only (not available on standalone Checkbox) The canonical `description` slot is associated via `aria-describedby` on the input. React Aria surfaces the description pattern through `<Text slot="description">` (from `react-aria-components`) but this is only wired automatically when `Checkbox` is a child of `CheckboxGroup`. Standalone `Checkbox` has no built-in description slot; consumers wishing to add description text to a standalone checkbox must wire `aria-describedby` manually. This is a structural constraint: the field-level description context is owned by the group container, not the individual checkbox. Source: https://react-aria.adobe.com/CheckboxGroup (fetched 2026-05-05)
anatomy[error-message] reshaped <FieldError> inside CheckboxGroup only (not available on standalone Checkbox) The canonical error-message slot is an `aria-live` region linked to the input via `aria-describedby` plus `aria-invalid`. React Aria's `<FieldError>` component (from `react-aria-components`) fulfils this role but is only wired into the validation context when used inside `CheckboxGroup`. Standalone `Checkbox` exposes `isInvalid` for the visual invalid state but provides no built-in error-message slot or aria-live region. Consumers must compose their own error-message element and wire `aria-describedby` manually for standalone usage. Source: https://react-aria.adobe.com/CheckboxGroup (fetched 2026-05-05)
axes.variants[group] reshaped CheckboxGroup (separate named component, not a variant prop) The canonical `group` variant documents that multiple checkboxes can be grouped under a shared label using `<fieldset>` + `<legend>`. React Aria ships this as a dedicated `CheckboxGroup` component (from `react-aria-components`) rather than as a `variant="group"` prop on `Checkbox`. `CheckboxGroup` renders a `<div>` (not a native `<fieldset>`) and manages group label, description, validation, and `aria-labelledby` wiring automatically. The semantic is preserved; the surface is a separate component, not a variant switch. Source: https://react-aria.adobe.com/CheckboxGroup (fetched 2026-05-05)
axes.properties[indeterminate] reshaped isIndeterminate (boolean — presentational only, persists regardless of user interaction) The canonical `indeterminate` property documents the tri-state where the indeterminate visual reflects aggregate child state and activating the parent checkbox cycles through checked/unchecked per the APG tri-state pattern. React Aria's `isIndeterminate` is explicitly documented as "presentational only — the indeterminate visual representation remains regardless of user interaction." This means React Aria does not implement the APG tri-state cycle automatically; the consumer must compute the mixed state from children and toggle `isIndeterminate` / `isSelected` externally. The prop exists but the activation-cycle contract is absent. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
axes.properties[disabled] reshaped isDisabled (renders aria-disabled="true" on the label, not HTML disabled on the input) The canonical `disabled` property maps to the HTML `disabled` attribute on the native input, which removes the element from the tab order. React Aria's `isDisabled` maps instead to `aria-disabled="true"` on the label root and suppresses press events via the `useCheckbox` hook, while keeping the element focusable (consistent with React Aria's global accessibility-first disabled contract, also applied in the Button implementation). Consumers who need the element removed from the tab order must additionally pass `excludeFromTabOrder`. Source: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/Checkbox.tsx (fetched 2026-05-05)
axes.properties[required] renamed isRequired (boolean on Checkbox and CheckboxGroup) One-to-one semantic match. React Aria adopts the `is*` naming convention uniformly for boolean state props. `isRequired` on standalone `Checkbox` sets `aria-required` on the hidden input and participates in the native Constraint Validation API. On `CheckboxGroup`, it requires at least one checkbox to be selected. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
axes.properties[invalid] renamed isInvalid (boolean on Checkbox and CheckboxGroup) One-to-one semantic match. React Aria's `is*` convention. `isInvalid` sets `data-invalid` on the root `<label>` and `aria-invalid` on the hidden input. It can be set manually or derived automatically from the `validate` function when `validationBehavior="aria"`. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
axes.properties[size] omitted React Aria `Checkbox` ships no `size` prop. React Aria is an unstyled primitive library; size scaling (sm / md / lg) is consumer CSS applied via class or data attributes. The `CheckboxRenderProps` type does not include a size field. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
axes.properties extended + isReadOnly (boolean) — React Aria ships an `isReadOnly` prop not present in the canonical. When set, the checkbox is visually active but the user cannot change its state; it still submits its value in forms. Sets `data-readonly` and `aria-readonly` on the input. The canonical has no equivalent — readonly binary fields are rare and the canonical documents only disabled as the inert state. React Aria models read-only as a distinct state from disabled because the ARIA spec distinguishes `aria-disabled` (cannot interact, may be excluded from form) from `aria-readonly` (can focus, cannot change, submits value). The canonical collapses these into `disabled` only, following HTML's form-control model where readonly is not applicable to `<input type="checkbox">`. React Aria adopts the broader ARIA model. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
axes.states.interactive[hover] extended + React Aria exposes `data-hovered` on the root `<label>` (via `useHover` internally), enabling pointer-device hover styling without relying on CSS `:hover`, which has known false-positive behaviour on touch devices. The canonical documents `hover` as an interactive state; React Aria surfaces it as `data-hovered` in addition to CSS `:hover`. React Aria normalises hover across pointer types. `data-hovered` reflects true hover (pointer device, not touch emulation). Consumers style from `[data-hovered]` for reliable cross-device hover feedback. This is additive over the canonical's CSS-only hover contract. Source: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/Checkbox.tsx (fetched 2026-05-05)
axes.states.data[checked] renamed data-selected (attribute) / isSelected (prop name) The canonical data state is `checked`; React Aria uses `isSelected` (the prop name) and surfaces it as `data-selected` (the CSS attribute). This is consistent with React Aria's pattern across all selection components (ListBox, RadioGroup, etc.) where "selected" is the uniform term. The ARIA mapping (`aria-checked="true"`) is still produced correctly by the library on the hidden input. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
axes.states.data[mixed] renamed data-indeterminate (attribute) / isIndeterminate (prop name) The canonical data state is `mixed` (aligning with `aria-checked="mixed"`). React Aria uses `isIndeterminate` (prop) and `data-indeterminate` (attribute). The underlying `aria-checked="mixed"` is still set on the hidden input by the `useCheckbox` hook. The rename is a React Aria naming preference; the AT-visible state is canonical-compliant. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
events[checkedChange] reshaped onChange(isSelected: boolean) The canonical `checkedChange` event carries `{ checked: boolean | 'mixed', previousChecked: boolean | 'mixed' }`. React Aria's `onChange` fires with a single `boolean` (the new `isSelected` value) — no `previousChecked` in the payload, and no `'mixed'` value since `isIndeterminate` is presentational-only and does not cycle through on user interaction. The indeterminate visual does not produce a `mixed` value in the `onChange` callback; consumers must manage the mixed-to-checked transition in their own `onChange` handler. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
events[validityChange] reshaped validate prop (function) + validationBehavior ("native" | "aria") The canonical `validityChange` event fires `{ valid, message }` when the field's validity changes. React Aria replaces the event model with a declarative approach: a `validate` function prop (returns a string or `true`) combined with `validationBehavior` to select between native browser Constraint Validation (`"native"`, default) and ARIA-only validation (`"aria"`). There is no `validityChange` event; validity state is derived reactively from `validate` return value and surfaced through `data-invalid` / `isInvalid` in `FieldError`. The trade-off shifts from imperative event handling to declarative validation logic. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
motion.durations reshaped data-selected / data-indeterminate CSS attribute selectors on the consumer's stylesheet React Aria ships no motion duration tokens. Indicator draw animation (checkmark stroke, indeterminate dash) and state-change transitions are driven by consumer CSS keyed off `[data-selected]` and `[data-indeterminate]` on the Checkbox root or the consumer's indicator element. The canonical `motion.duration.indicatorDraw` and `motion.duration.stateChange` tokens are achievable but entirely consumer-side. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
motion.reducedMotionFallback omitted React Aria does not implement reduced-motion suppression in JS. Consumers apply `@media (prefers-reduced-motion: reduce)` in their CSS to suppress transitions on `[data-selected]` and `[data-indeterminate]`. The library's data-attribute state machine still updates; motion suppression is entirely consumer CSS responsibility. Source: https://react-aria.adobe.com/Checkbox (fetched 2026-05-05)
Why this audit reads the way it does

React Aria Checkbox is a behaviour-only primitive: it owns the accessibility contract (aria-checked, indeterminate, form participation, press event normalisation, focus management) while leaving visual slot anatomy, size variants, and motion timing entirely to the consumer. The three deepest structural divergences from the canon are: 1. No named indicator, label, description, or error-message slots — all visual sub-structure is consumer-composed children. Description and FieldError wiring is only automatic inside CheckboxGroup; standalone Checkbox consumers wire aria-describedby manually. 2. isIndeterminate is presentational-only — it does not implement the APG tri-state activation cycle. The canonical contract (mixed → checked on activation) must be coded by the consumer by resetting isIndeterminate and isSelected in the onChange handler. 3. isDisabled always maps to aria-disabled (never HTML disabled) — consistent with React Aria's global accessibility-first disabled contract. The canonical dual-mode model (disabled removes from tab order; aria-disabled keeps focusable) is collapsed: React Aria always keeps the element focusable; consumers opt out via excludeFromTabOrder. The rename surface (isSelected/data-selected for "checked", isIndeterminate/data-indeterminate for "mixed", isRequired, isInvalid, isDisabled) follows React Aria's consistent is*/data-* naming scheme applied uniformly across all components. The underlying AT-visible ARIA state (aria-checked, aria-required, aria-invalid, aria-disabled) is canonical- compliant.

Designer

Figma anatomy

Slot Figma type Hint
root frame Auto-layout horizontal frame; input + label inline; description + error stacked below
input frame Square frame; size variant binds to size token; carries focus ring on focus-visible
indicator instance Icon component; check / dash / none variants per data state
label text Text element; size matches root size variant; weight regular by default
description text Smaller text below label; muted color; visibility per "has description" property
error-message text Error-toned text below label/description; visibility per "has error" property
Designer

Token usage per slot

root
spacing
  • gapspacing.tight
input
radius
  • cornerradius.sm
color
  • bordercolor.border.strong
indicator
color
  • foregroundcolor.text.inverse
label
color
  • foregroundcolor.text.primary
typography
  • sizetext.sm
description
color
  • foregroundcolor.text.muted
typography
  • sizetext.xs
error-message
color
  • foregroundcolor.text.danger
typography
  • sizetext.xs
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantsingle / group. group is the parent "select all" tri-state pattern.
SizeEnumsizesm / md / lg.
CheckedBooleancheckedControlled state. Radix accepts `boolean | "indeterminate"`; React Aria pairs `isSelected` with `isIndeterminate`. Renders the indicator glyph in the indicator slot.
IndeterminateBooleanindeterminateRuntime-only — set via `inputElement.indeterminate = true` in JS, never as an HTML attribute. Browser surfaces it as `aria-checked="mixed"`. Indicator slot renders the dash glyph.
RequiredBooleanrequiredPairs visual asterisk with the `required` attribute. Both must ship together.
DisabledBooleandisabled
InvalidBooleaninvalidPairs `aria-invalid="true"` with the visual error treatment.
LabelTextchildren of `<label>`Visible label text. Required slot — never omit, hide visually if needed.
DescriptionTextdescriptionOptional supporting prose, programmatically linked via `aria-describedby`.
Error MessageTexterrorMessageVisibility bound to invalid state.
IndicatorSlotindicatorDecorative — checkmark when checked, dash when indeterminate, empty when unchecked.
NameTextnameForm-field key under which the value submits. Required for form participation.
Designer

Motion

TransitionDuration token
indicatorDrawmotion.duration.fast
stateChangemotion.duration.fast
Easing
motion.easing.standard
Reduced motion
Instant (jump cut)
Both

Internationalisation

RTL · mirroring

Checkbox + label inline order follows writing direction — input on the inline-start, label on the inline-end (LTR: left + right; RTL: right + left). Use `flex-direction: row` with logical properties; the browser handles the swap automatically. Indicator glyph is direction-neutral (checkmark and dash are symmetric or non-directional). Description and error-message inherit the writing direction; SR announces label-first regardless because the programmatic association is order-independent.

Text expansion

Label text expands 30-50% under translation (English "Subscribe" → German "Abonnieren" 25% longer; English "I accept the terms" → French "J'accepte les conditions" 20% longer). Reserve no fixed width on the label slot; let the wrapper flow and wrap to multiple lines under pressure. The input slot stays size-token-bound and never text-expands. Group `<legend>` follows the same expansion pattern as the standalone label.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

single group

Properties

The same component, parameterised.

PropertyType
size sm | md | lg
indeterminate boolean
required boolean
disabled boolean
invalid boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
uncheckedcheckedmixedinvalid
Both

State transitions

FromToTrigger
uncheckedcheckedUser activates input (click on label, Space when input focused)
checkeduncheckedUser activates input (click on label, Space when input focused)
mixedcheckedUser activates parent group-checkbox (canonical "select all" path — APG tri-state)
mixeduncheckedUser activates parent group-checkbox in alternative APG tri-state — "uncheck all"
Both

Figma↔Code mismatches

  1. 01
    Figma

    Indeterminate drawn as a separate "indeterminate" component variant

    Code

    Indeterminate is a runtime JS-only property, never an HTML attribute

    Consequence

    Designers ship "Checkbox / Indeterminate" as a peer of "Checkbox / Checked"; developers know `indeterminate` is set via `inputElement.indeterminate = true` in JS — it is not in the HTML markup. The two surfaces look like they declare the same state but the design file misses the JS-only semantic, leading to "why does my Figma variant not exist as a prop?" friction.

    Correct

    Document indeterminate as a runtime data-state, not a static variant. Figma uses a "Checkbox / State / Mixed" variant labelled with a footnote ("set via JS at runtime") so the designer-handoff carries the implementation contract. Code uses `indeterminate` property; the browser surfaces it as `aria-checked="mixed"` automatically.

  2. 02
    Figma

    Checkbox hit-target painted as the 16×16 visual square

    Code

    Hit-target extends to the wrapping label (≥ 24×24 effective tap area)

    Consequence

    Designers draw the visual square at 16×16 px and assume that is the click target; developers extend the hit-target via the wrapping label and via padding. The two surfaces look identical but the actual tap area differs. Design QA sometimes "tightens" the label margin away from the checkbox visually and breaks the WCAG 2.5.8 24×24 threshold without realising.

    Correct

    Document the hit-target as the wrapping label region (input + label + adjacent padding). Figma's component includes an invisible bounding box at ≥ 24×24 px that encompasses the visual square. Code wraps the input in `<label>` so the entire region is clickable.

  3. 03
    Figma

    "Select all" parent shown as a regular checked checkbox when partial

    Code

    Parent reflects mixed state when child set is partial (`aria-checked="mixed"`)

    Consequence

    Designers ship the "select all" parent in two states (checked / unchecked); developers compute three states (all / none / partial). The Figma file does not illustrate the partial state, so designers cannot review it; QA misses the missing dash glyph until SR users report "select all is misleading".

    Correct

    Document the parent-child mixed-state propagation as a canonical contract. Figma carries the three-state parent variant (checked / unchecked / mixed). Code computes the parent from the children via `aria-checked = all-checked ? "true" : none-checked ? "false" : "mixed"` and renders the dash glyph for mixed.

  4. 04
    Figma

    Label drawn as inline text disconnected from the input

    Code

    Label associated via `<label for>` or `<label>`-wrap of the input

    Consequence

    Designers draw the label as a sibling text node; developers ship the label as a `<label>` with `for` attribute or as the wrapping element. The visual is the same but the programmatic association is what AT relies on. Designers sometimes split label text across multiple inline elements (price + currency, primary + secondary lines) and break the labelling contract.

    Correct

    Document the label as a single semantic unit in the anatomy. Figma carries one label slot; complex labels are composed inside the slot but live under one programmatic `<label>` boundary in code.

Both

Contracts

Non-negotiable contracts

  1. APGAPG: Checkbox pattern

    The input has a programmatically-associated label — either `<label for="<id>">` or a wrapping `<label>`. Adjacent prose, placeholder text, and visual proximity do not satisfy the contract. The label is the canonical accessible name.

    Without the association, SR users hear "checkbox, not checked" with no field meaning. The programmatic contract is what AT relies on; visual layout is sighted-only and breaks under voice navigation, mobile SR, and accessibility tooling.

  2. APGAPG: Checkbox tri-state

    Parent group-checkbox reflects the mixed (indeterminate) state when its child set is partial. Set `parent.indeterminate = anyChecked && !allChecked`; the browser surfaces this as `aria-checked="mixed"` and the indicator slot renders the dash glyph. Implementations that show checked or unchecked when partial lie about state.

    Without the mixed propagation, the "select all" pattern becomes misleading — users see a checked parent but only some children are selected. SR users hear contradictory state on parent activation. The tri-state contract is a canonical APG requirement.

  3. WCAGWCAG 2.5.8 Target Size (Minimum)

    Hit-target is ≥ 24×24 CSS pixels (WCAG 2.5.8 Level AA; AAA threshold is 44×44). The visual square may be smaller, but the clickable region — typically the wrapping label — must meet the threshold.

    Below the threshold, mobile users and users with motor- control limitations cannot reliably activate the checkbox. The visual square is misleading — designers often paint 16×16 squares thinking that is the target. Verify the effective hit-target, not the painted size.

Vocabulary drift

HTML
`<input type="checkbox">`
Native HTML element. `indeterminate` is a JS-only property (not an HTML attribute). Default submitted value is `"on"` when no `value` attribute is set. Only checked checkboxes participate in form submission.
WAI-ARIA
`role="checkbox"` (with `aria-checked` true | false | mixed)
WAI-ARIA tri-state checkbox pattern. The native HTML input provides the role implicitly; custom-rendered checkboxes must set `role` plus `aria-checked` plus Space-key handling manually.
Material 3
Checkbox
Material 3 uses the canonical name. Sizes converge on a single "checkbox" with size-token variation.
GOV.UK
Checkboxes
GOV.UK uses plural "Checkboxes" because the canonical usage is grouped (`<fieldset>` + `<legend>`). Single- checkbox usage is also documented but the plural framing nudges authors toward the group form.
Polaris
Checkbox
Polaris adds a `helpText` slot that maps to the canonical description. The Polaris Checkbox does not ship a built-in error-message slot; consumers compose it.
Designer

Common mistakes

Blocker

#checkbox-no-label-assoc

Input has no programmatically-associated label

Problem

The input renders with adjacent text but no `<label for>`, no wrapping `<label>`, and no `aria-labelledby`. SR users hear "checkbox, not checked" without the field meaning; pointer users may have a working hit-target by accident but keyboard / AT users cannot identify the field.

Fix

Wrap the input in `<label>` (the input becomes a child of the label) or pair the input with `<label for="<id>">` as a sibling. Either form establishes the canonical association. Never substitute placeholder text, adjacent paragraphs, or visual proximity — the AT contract is programmatic.

Blocker

#checkbox-mixed-not-set-on-partial

Parent group-checkbox does not reflect mixed state when children are partial

Problem

The "select all" parent shows checked or unchecked but never the mixed (indeterminate) visual. Users who select a subset of children see the parent as either fully checked (lying about state) or fully unchecked (also lying). SR users hear contradictory state on activation ("checked, partially checked").

Fix

Compute the parent's `indeterminate` property from the children. Set `parent.indeterminate = anyChecked && !allChecked`. The browser surfaces this as `aria-checked="mixed"` and the indicator slot renders the dash glyph. APG tri-state pattern is the canonical reference.

Major

#checkbox-target-too-small

Hit-target is below the WCAG 2.5.8 24×24 threshold

Problem

The clickable area (visual square plus surrounding margin) is below 24×24 CSS pixels. WCAG 2.5.8 (Target Size, Level AA in WCAG 2.2) requires ≥ 24×24; the AAA threshold is 44×44. Mobile users and users with motor-control limitations miss the target — checkbox toggle becomes pointer-precision-dependent.

Fix

Extend the hit-target via the wrapping `<label>`. The visual square may stay 16×16 or 18×18 for typographic density, but the clickable label region is ≥ 24×24. Verify with browser DevTools layout inspector or automated hit-target audits — the visual square is misleading.

Major

#checkbox-placeholder-as-label

Field meaning lives in placeholder text or adjacent paragraph

Problem

The checkbox has no associated label; the field meaning is in adjacent paragraph text (or, more rarely, in placeholder-styled text near the input). SR users hear "checkbox" with no semantic. Sighted users can read the adjacent text but the programmatic accessible name is empty.

Fix

Author a `<label>` with the field meaning. Adjacent explanation prose belongs in the description slot, programmatically linked via `aria-describedby`. The label is the canonical accessible name; the description is supplementary.

Major

#checkbox-color-only-state

Checked state communicated via colour change alone

Problem

The checked state is signalled only by a colour change of the input border (e.g. light grey when unchecked, blue when checked) without an indicator glyph. Colour-blind users and users in low-light or grayscale display modes cannot distinguish the states. Fails WCAG 1.4.1 (Use of Colour).

Fix

Always render the indicator glyph in the checked state — a checkmark for `true`, a dash for `mixed`, none for `false`. The glyph is the primary state signal; colour is secondary. Test in grayscale (`filter: grayscale(1)`) to verify the state reads.

Accessibility hints
Slot Accessibility hint
root When the root is `<label>`, the input becomes the accessible name's host automatically. When the root is `<div>`, the inner `<label for="<input-id>">` provides the association. Never leave the input without an associated label — placeholder text and adjacent prose are not equivalent to a programmatically-linked label.
input `<input type="checkbox">` provides `role="checkbox"` natively. `aria-checked` is set automatically by the browser to `"true"` or `"false"`; for indeterminate the DOM `indeterminate` property is set via JS, which the browser surfaces as `aria-checked="mixed"`. Never set `aria-checked` manually on a native input. Custom-rendered checkboxes (built from `<button>`) must set `role="checkbox"` plus `aria-checked` manually plus listen for Space-key toggle.
indicator Decorative — `aria-hidden="true"` (or rendered via SVG without a `role`). The state is announced via the input's `aria-checked` value, never via the indicator glyph. SR users hear "checkbox, checked" / "checkbox, not checked" / "checkbox, partially checked"; they do not hear "checkmark icon".
label The label text is the input's accessible name. SR users hear "checkbox, Subscribe to newsletter, not checked". Avoid using just "yes" / "no" — the field meaning must be in the label. For grouped checkboxes, the parent `<fieldset>` carries a `<legend>` describing the group; each checkbox's label describes its specific option.
description Associate via `aria-describedby` on the input. SR announces description after the label and the checked state. Avoid putting required-field markers in the description — use `required` attribute / `aria-required` on the input.
error-message `aria-describedby` includes the error-message id; `aria-invalid="true"` on the input triggers SR error announcement on focus. The error-message element itself carries `aria-live="polite"` so async-validation errors announce when they appear without forcing focus moves.