---
title: "Checkbox"
description: "A single labelled box a reader ticks, unticks or leaves in a mixed state, built on Base UI Checkbox and carrying neither colour axis."
url: "https://opsinjs.pensievelabs.org/components/checkbox"
source: "https://opsinjs.pensievelabs.org/components/checkbox.md"
section: "Components"
status: "shipped"
kind: "component"
category: "actions-and-forms"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["tick box", "multi select"]
usedIn: ["diabetes-medicines-app"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="checkbox"
  status="shipped"
  questions="[
  &#x22;No contrast pair is measured here: the unticked hairline and the description ink against the page ground are argued, not checked.&#x22;,
  &#x22;Nobody has listened to the box in a screen reader, so whether aria-checked=\&#x22;mixed\&#x22; is heard as partly on is reasoned, not heard.&#x22;,
  &#x22;Forced colours are unhandled: the fill and the hairline both become the platform pair, and whether ticked, unticked and mixed stay distinct is untested.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="checkbox" />

## Installation [#installation]

<ComponentInstall name="checkbox" unbuilt="false" importPath="@/components/ui/checkbox" dependencies="[&#x22;@base-ui/react&#x22;, &#x22;lucide-react&#x22;]" />

## Usage [#usage]

```tsx
import { Checkbox } from "@/components/ui/checkbox"
```

```tsx
<Checkbox
  label="Email me a copy"
  checked={agreed}
  onCheckedChange={setAgreed}
/>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;A single option a reader turns on or off, such as an optional agreement.&#x22;,
  &#x22;Any-number-including-none choices across a short list, where each row is its own box.&#x22;,
  &#x22;The head of such a list, where a parent box uses the mixed state for the some case.&#x22;,
]"
  avoid="[
  { case: &#x22;Exactly one option must be chosen. Checkboxes let a reader pick two answers to a one-answer question.&#x22;, instead: &#x22;radio-group&#x22; },
  { case: &#x22;The label has to say what a reading means or how urgent it is.&#x22;, instead: &#x22;status-pill&#x22; },
  { case: &#x22;The box has to join a form's label, hint, error and validity as one unit.&#x22;, instead: &#x22;field&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="checkbox"
  parts="[
  {
    name: &#x22;Checkbox&#x22;,
    describes: &#x22;The root wrapper. Neutral chrome only, on neither colour axis.&#x22;,
    prop: &#x22;label&#x22;,
  },
  {
    name: &#x22;Checkbox.Control&#x22;,
    describes: &#x22;The box, a Base UI Checkbox.Root with role=\&#x22;checkbox\&#x22;, aria-checked and a hidden form input.&#x22;,
    prop: &#x22;checked&#x22;,
  },
  {
    name: &#x22;Checkbox.Indicator&#x22;,
    describes: &#x22;The mark, mounted only while ticked or mixed: a lucide Check when ticked, a Minus when mixed.&#x22;,
  },
  {
    name: &#x22;Checkbox.Label&#x22;,
    describes: &#x22;The visible words. The label element wraps the box, so a tap anywhere on the row toggles it.&#x22;,
    prop: &#x22;label&#x22;,
  },
  {
    name: &#x22;Checkbox.Description&#x22;,
    describes: &#x22;Optional guidance beneath the label, tied to the box with aria-describedby.&#x22;,
    prop: &#x22;description&#x22;,
  },
]"
/>

<CompositionTree
  name="checkbox"
  tree="[
  {
    part: &#x22;Checkbox&#x22;,
    cardinality: &#x22;1&#x22;,
    note: &#x22;The wrapper&#x22;,
    children: [
      {
        part: &#x22;Checkbox.Control&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;role=\&#x22;checkbox\&#x22;, wrapped by the label&#x22;,
        children: [
          {
            part: &#x22;Checkbox.Indicator&#x22;,
            cardinality: &#x22;0..1&#x22;,
            note: &#x22;Mounted only when ticked or mixed&#x22;,
          },
        ],
      },
      {
        part: &#x22;Checkbox.Label&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;Wraps the control&#x22;,
      },
      {
        part: &#x22;Checkbox.Description&#x22;,
        cardinality: &#x22;0..1&#x22;,
        note: &#x22;Present when description is set&#x22;,
      },
    ],
  },
]"
/>

## Examples [#examples]

### An optional agreement [#an-optional-agreement]

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

<ComponentPreview name="checkbox-an-optional-agreement" kind="example" align="start" />

### A three-state parent [#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.

<ComponentPreview name="checkbox-a-three-state-parent" kind="example" align="start" />

## Content guidelines [#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](./status-pill.mdx), never a label.

<DoDont>
  <DoDont.Do>
    **"Email me a copy"** ticks to a plain yes, with the detail in the description.
  </DoDont.Do>

  <DoDont.Dont>
    **"Do not exclude me from reminders"** makes an empty box a double negative.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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.

<KeyboardTable
  name="checkbox"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Moves focus onto the box&#x22;,
    notes: &#x22;One tab stop. A disabled box is skipped.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Moves focus back off the box&#x22;,
    notes: &#x22;One stop in reverse too.&#x22;,
  },
  {
    keys: &#x22;Space&#x22;,
    action: &#x22;Toggles the box&#x22;,
    notes: &#x22;A mixed box becomes ticked and reports true.&#x22;,
  },
  {
    keys: &#x22;Enter&#x22;,
    action: &#x22;Does nothing&#x22;,
    notes: &#x22;A box at the foot of a form does not toggle on submit.&#x22;,
  },
  {
    keys: &#x22;Pointer on the label&#x22;,
    action: &#x22;Toggles the box&#x22;,
    notes: &#x22;Anywhere along the row, not only the box.&#x22;,
  },
]"
/>

<ContrastReport component="checkbox" />

## Data attributes [#data-attributes]

<DataAttributesTable
  name="checkbox"
  rows="[
  {
    attribute: &#x22;data-slot&#x22;,
    condition: &#x22;On the root and every inner part&#x22;,
    value: &#x22;checkbox, checkbox-control, checkbox-indicator, checkbox-label, checkbox-description&#x22;,
  },
  {
    attribute: &#x22;data-checked&#x22;,
    condition: &#x22;On Checkbox.Control while ticked&#x22;,
    value: &#x22;Set by Base UI&#x22;,
  },
  {
    attribute: &#x22;data-unchecked&#x22;,
    condition: &#x22;On Checkbox.Control while neither ticked nor mixed&#x22;,
    value: &#x22;Set by Base UI&#x22;,
  },
  {
    attribute: &#x22;data-indeterminate&#x22;,
    condition: &#x22;On Checkbox.Control while mixed&#x22;,
    value: &#x22;Set by Base UI&#x22;,
  },
  {
    attribute: &#x22;data-disabled&#x22;,
    condition: &#x22;On Checkbox.Control while disabled&#x22;,
    value: &#x22;Set by Base UI&#x22;,
  },
]"
/>

## API reference [#api-reference]

<PropsTable name="CheckboxProps" />

`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 [#related]

* [RadioGroup](./radio-group.mdx) is for a choice where exactly one option can be chosen.
* [Switch](./switch.mdx) turns one thing on or off immediately, and has no mixed state.
* [Field](./field.mdx) wires a control to its label, hint, error and validity as one unit.
* [StatusPill](./status-pill.mdx) is where a clinical level lives.
