opsinjs
ComponentsActions and forms

Checkbox

A single labelled box a reader ticks, unticks or leaves in a mixed state, built on Base UI Checkbox and carrying neither colour axis.

Preview

Theme
Density
Text
Status
checkbox · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/checkbox

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Checkbox } from "@/components/ui/checkbox"
<Checkbox
  label="Email me a copy"
  checked={agreed}
  onCheckedChange={setAgreed}
/>

When to use it

Use it when

  • A single option a reader turns on or off, such as an optional agreement.
  • Any-number-including-none choices across a short list, where each row is its own box.
  • The head of such a list, where a parent box uses the mixed state for the some case.

Do not use it when

  • Exactly one option must be chosen. Checkboxes let a reader pick two answers to a one-answer question.Use radio-group instead.
  • The label has to say what a reading means or how urgent it is.Use status-pill instead.
  • The box has to join a form's label, hint, error and validity as one unit.Use field instead.

Anatomy

  1. CheckboxThe root wrapper. Neutral chrome only, on neither colour axis.Controlled by label
  2. Checkbox.ControlThe box, a Base UI Checkbox.Root with role="checkbox", aria-checked and a hidden form input.Controlled by checked
  3. Checkbox.IndicatorThe mark, mounted only while ticked or mixed: a lucide Check when ticked, a Minus when mixed.
  4. Checkbox.LabelThe visible words. The label element wraps the box, so a tap anywhere on the row toggles it.Controlled by label
  5. Checkbox.DescriptionOptional guidance beneath the label, tied to the box with aria-describedby.Controlled by description
  • Checkbox 1The wrapper
    • Checkbox.Control 1role="checkbox", wrapped by the label
      • Checkbox.Indicator 0..1Mounted only when ticked or mixed
    • Checkbox.Label 1Wraps the control
    • Checkbox.Description 0..1Present when description is set

Examples

An optional agreement

A lone box with label and description. The description says what ticking does.

Theme
Density
Text
Status
checkbox-an-optional-agreement · base base · style base-lyraOpen under the product theme

A three-state parent

The example passes checked="indeterminate" when some but not all children are ticked, so the box draws a Minus, not a Check.

Theme
Density
Text
Status
checkbox-a-three-state-parent · base base · style base-lyraOpen under the product theme

Content guidelines

Phrase the label so a tick means yes. Keep it to a short line and put what ticking does in description. A clinical status word is a StatusPill, never a label.

Do

"Email me a copy" ticks to a plain yes, with the detail in the description.

Don’t

"Do not exclude me from reminders" makes an empty box a double negative.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending.

What was fixed. Base UI draws the box as a span with role="checkbox", and the wrapping label named only the hidden input, so the box had no accessible name (WCAG 4.1.2). The visible label carries an id and the box points at it with aria-labelledby, so the name is the visible words verbatim (WCAG 2.5.3). The fix reaches the demo and both examples.

What holds. The description is tied with aria-describedby, colour is never the sole state carrier because fill, border and the Check against Minus glyph all move together, and sizes are rem.

Residual. The rendered probe measured the inner box at 20 by 20, under SC 2.5.8, but the pointer target is the 44px label row that toggles anywhere along it, so it clears the house floor. The colour transition moves no transform and needs no reduced-motion guard. Contrast pairs are not yet measured.

KeyActionNotes
TabMoves focus onto the boxOne tab stop. A disabled box is skipped.
Shift+TabMoves focus back off the boxOne stop in reverse too.
SpaceToggles the boxA mixed box becomes ticked and reports true.
EnterDoes nothingA box at the foot of a form does not toggle on submit.
Pointer on the labelToggles the boxAnywhere along the row, not only the box.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every inner partcheckbox, checkbox-control, checkbox-indicator, checkbox-label, checkbox-description
data-checkedOn Checkbox.Control while tickedSet by Base UI
data-uncheckedOn Checkbox.Control while neither ticked nor mixedSet by Base UI
data-indeterminateOn Checkbox.Control while mixedSet by Base UI
data-disabledOn Checkbox.Control while disabledSet by Base UI

API reference

Prop

Type

Generated from CheckboxProps in registry/bases/base/checkbox.tsx.

label is required. checked accepts true, false or "indeterminate"; omitted, the box is uncontrolled from an unticked start. indeterminate is the other route to the mixed state and wins when both are given. onCheckedChange reports a boolean, so a mixed box reports true when ticked.

  • RadioGroup is for a choice where exactly one option can be chosen.
  • Switch turns one thing on or off immediately, and has no mixed state.
  • Field wires a control to its label, hint, error and validity as one unit.
  • StatusPill is where a clinical level lives.

On this page