Skip to content

checkbox

checkbox declares that an element is a binary (or indeterminate) selection control. Without it the control carries aria-selected — a listbox-option attribute — and an inert aria-checked. It cannot be focused, checked, submitted, or validated; the label is an unassociated sibling; the error message rendered only when invalid is announced to no one; and the :checked, :disabled, and :indeterminate selectors the css transformer emits match nothing.

Status — React: Implemented • Web Components: Implemented • iOS: Not yet planned • Android: Not yet planned

Roles

Apply the following roles to elements:

RoleTypeElement
checkboxControlElement of type: container or type: glyph
labelPartElement of type: text or nested instance prop of type: string
descriptionPartElement of type: text or nested instance prop of type: string
errormessagePartElement of type: text or type: instance
indicatorPartElement of type: container, type: glyph, or type: vector
ElementEmitted as
Injected first siblingVisually hidden <input type="checkbox"> — carries the states and the contract
The role element<label htmlFor> pointing at the input, keeping its class and data-element, aria-hidden

The proxy’s whole footprint activates the input through HTML’s own label behavior, with no positioning CSS — aria-hidden silences announcement but not pointer events. The proxy carries only the drawn state, never the label text: the accessible name comes from a second <label> emitted by the element carrying the label part, and HTML permits multiple labels per control.

  • A label part must not sit inside the proxy’s subtree — labels do not nest, and resolution warns if one does
  • A glyph carrying the role is re-hosted inside the proxy structure first
  • The injection adds one sibling, so positional CSS (:nth-child, adjacent-sibling selectors) in that subtree needs review
  • The stylesheet gives the proxy cursor: pointer

States

The checkbox role is typically applied in conjunction with the following states:

StateEffectClassify?
checkedNative checked, flipped by the wired handlerRecommended
indeterminateNative .indeterminate DOM property, set via a ref effectRecommended
disabledNatively disabled, enforced by the platformRecommended
requiredNative requiredRecommended
invalidaria-invalid on the inputRecommended
hover / activeNative, on the proxy inputRecommended, if the library styles it
focus / focus-visiblePlatform ring, re-drawn on the proxyOptional — prefer the platform default

The platform draws its ring around the focused element, which is the hidden input with no visible box. The generated stylesheet re-draws it on the visible proxy:

.checkbox__control-input:focus-visible + .checkbox__control {
outline: auto;
outline-offset: 2px;
}

outline-style: auto asks for the platform’s own ring rather than imitating it. The input is injected immediately before the proxy, so the adjacent-sibling selector holds by construction. Override the same selector for a custom indicator.

Read more about states in specs.

Specs

Component anatomy typically has elements and roles like:

anatomy:
control:
type: container
role: checkbox
checkGlyph:
type: glyph
role: indicator
formLabel:
type: instance
instanceOf: formLabel
role: [label]
errorMessage:
type: instance
instanceOf: formErrorMessage
role: [errormessage]

Figma

Annotate the following layers:

  • control as role:checkbox — on the layer drawing the box, and nothing beyond it
  • the check mark as role:indicator
  • label as role:label on a text element or through a nested instance
  • the validation message as role:errormessage

The label must not sit inside the role element’s subtree — the proxy becomes a <label>, and labels do not nest.

React

Implementation

<Checkbox label="Send me trip updates" checked={on} onChange={(e) => setOn(e.target.checked)} />

Before / After

// before
<div className="checkbox" data-element="root"
aria-disabled={p.disabled ? 'true' : undefined}
aria-selected={p.selected ? 'true' : undefined}>
<div className="checkbox__control" data-element="control">{/* … */}</div>
{/* … unassociated label and error message … */}
</div>
// after
<div className="checkbox" data-element="root">
<input id={controlId} type="checkbox"
checked={selected === "selected"}
onChange={(e) => { setSelected(e.target.checked ? "selected" : "unselected"); p.onChange?.(e); }}
disabled={p.disabled} name={p.name} value={p.value} />
<label className="checkbox__control" data-element="control" htmlFor={controlId} aria-hidden="true">{/* … */}</label>
{/* … label routed htmlFor={controlId}; error message gains id + aria-describedby … */}
</div>

The proxy becoming a label is the single most important detail: without it the visual looks correct and does nothing on click.

Contract

PropTypeTierGenerated body
onChange?(e: ChangeEvent<HTMLInputElement>) => voidMUSTWired — flips checked, then calls the prop
onBlur?(e: FocusEvent) => voidSHOULDStub
name?stringMUST— form submission identity
value?stringMUST— submitted value
checkedboolean—The existing variant prop

name and value are genuinely new API the spec does not declare.

Web Components

Same proxy structure inside the shadow root. Shadow DOM hides the inner input from a containing form, so the host participates directly: it emits static formAssociated = true, attaches ElementInternals, and reports its value and validity through them. The sync runs from willUpdate, so both user interaction and consumer-driven prop changes reach the form.

iOS

Not yet planned. Intended binding:

TypeToggle
AnnouncedThe label and checked value; double-tap flips it
indeterminateNo native equivalent — degrades to unchecked with a warning
indicatorNot consumed — the platform draws its own mark

Android

Not yet planned. Intended binding:

TypeCheckbox with Role.Checkbox
Announced”Checked” / “not checked”; double-tap toggles
indeterminateTriStateCheckbox with ToggleableState.Indeterminate
indicatorPartly consumed — CheckboxDefaults.colors takes box and mark colors

Additional details

Enum-valued state props

A checked fact is boolean, but libraries routinely carry it in a three-value enum (unselected / selected / indeterminate). The mapping is declared entirely by the states classification, and the rule applies to every wired role reading an enum prop, not just this one.

RuleDetail
Naming the valueselected: { prop: selected, value: Selected }. Without a value, the concept’s own name is the value — checked: { prop: state } means state === "checked"
NormalizationEnum values are lowercased in the emitted contract, and every generated comparison matches that spelling
Write-backA boolean prop is assigned directly; an enum prop is written to the declared checked value on check and the remaining arm on uncheck
Tri-stateThe flip only rewrites the checked/unchecked pair, so an indeterminate value set by the consumer survives until the user operates the control

Nothing here is decided by the transform — which prop, and which value counts as checked, both come from the classification.

Wired state

onChange follows the wired state model: internal state seeded from the checked prop, flipped on activation, consumer callback after.

Prerequisite: a checked classification naming the prop. Without it the handler degrades to a stub and warns.

Accessible name

From the label part, emitted as a real <label> associated by htmlFor. The click-target proxy is aria-hidden and contributes nothing.

See also

  • radio — exclusive selection, styled in place rather than proxied
  • switch — the same structure, announced as a switch
  • indicator — the check glyph
  • Roles overview — the vocabulary and how roles are authored