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
checkbox · base base · style base-lyraOpen under the product themeInstallation
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-groupinstead. - The label has to say what a reading means or how urgent it is.Use
status-pillinstead. - The box has to join a form's label, hint, error and validity as one unit.Use
fieldinstead.
Anatomy
CheckboxThe root wrapper. Neutral chrome only, on neither colour axis.Controlled bylabelCheckbox.ControlThe box, a Base UI Checkbox.Root with role="checkbox", aria-checked and a hidden form input.Controlled bycheckedCheckbox.IndicatorThe mark, mounted only while ticked or mixed: a lucide Check when ticked, a Minus when mixed.Checkbox.LabelThe visible words. The label element wraps the box, so a tap anywhere on the row toggles it.Controlled bylabelCheckbox.DescriptionOptional guidance beneath the label, tied to the box with aria-describedby.Controlled bydescription
- 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
- Checkbox.Control 1role="checkbox", wrapped by the label
Examples
An optional agreement
A lone box with label and description. The description says what ticking does.
checkbox-an-optional-agreement · base base · style base-lyraOpen under the product themeA 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.
checkbox-a-three-state-parent · base base · style base-lyraOpen under the product themeContent 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.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves focus onto the box | One tab stop. A disabled box is skipped. |
| Shift+Tab | Moves focus back off the box | One stop in reverse too. |
| Space | Toggles the box | A mixed box becomes ticked and reports true. |
| Enter | Does nothing | A box at the foot of a form does not toggle on submit. |
| Pointer on the label | Toggles the box | Anywhere along the row, not only the box. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On the root and every inner part | checkbox, checkbox-control, checkbox-indicator, checkbox-label, checkbox-description |
| data-checked | On Checkbox.Control while ticked | Set by Base UI |
| data-unchecked | On Checkbox.Control while neither ticked nor mixed | Set by Base UI |
| data-indeterminate | On Checkbox.Control while mixed | Set by Base UI |
| data-disabled | On Checkbox.Control while disabled | Set 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.
Related
- 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.