Skip to documentation content

Checkbox

Let people turn independent options on or off or select several related choices.

checkbox-demo

Overview

Use a checkbox when each option stands on its own and any combination of states is valid: preferences, permissions, filters, notification channels, optional add-ons. Because every checkbox toggles independently, people can select none, some, or all of a set without breaking anything.

Use a radio group when exactly one option must be chosen from a visible set — see Don't do this for what happens when the two get mixed up. Use Switch for a single setting that takes effect immediately rather than as part of a form submission.

CheckboxGroup shares one value across several checkboxes and ships in the same file. It is covered under Examples below.

Anatomy

A checkbox has an input, a visual indicator, a label, an optional description, and a state. The indicator draws a checkmark when checked and a dash when indeterminate; both are inline SVGs so they inherit text color and scale with the control. The label should make sense read on its own, because screen readers announce checkboxes by their label text alone.

A group holds several checkboxes that answer one question and submits one shared array value. A group needs its own visible label — through aria-labelledby, a Fieldset legend, or surrounding heading — so people know what the choices are about before reading the first option.

States

Checked / unchecked. Checked boxes fill with the accent background and drop their border. Unchecked boxes show a bordered surface with a subtle hover state.

Indeterminate. Set indeterminate to show the dash. The hidden input receives aria-checked="mixed", so screen readers announce it as partially checked rather than silently disagreeing with the visual. Indeterminate is a display state: clicking an indeterminate checkbox checks or unchecks it like any other toggle, so your code decides what happens to the children.

Disabled. Disabled items keep their current visual state but dim to half opacity, show a not-allowed cursor, are skipped by keyboard focus, and submit no value. Use them for options that are visible but unavailable in the current context.

Read-only. readOnly keeps the checkbox in the tab order and submits its value but ignores clicks, with a default cursor and reduced opacity. Prefer disabled for unavailable options; prefer read-only when the value exists and must be submitted but must not change here.

Invalid. Setting aria-invalid="true" switches the border and checked fill to the danger colors, so validation reaches assistive technology as well as the eye. Pair it with error text that says what to fix.

Accessibility

The checkbox renders a span with role="checkbox" plus a visually hidden native input, wrapped in a Label. Press Space to toggle; Enter also activates it because Base UI gives the element button-style key handling. Focus moves with Tab and shows a visible ring only for keyboard focus (focus-visible).

Always wrap the checkbox in a Label (or connect one with htmlFor). Text sitting next to a checkbox without label association is invisible to screen readers and does not extend the click target. Wrapping in a Label makes the whole phrase clickable, which matters more than the control's own size.

The rendered box is 16 px (size="large") or 12 px (`size="small") — well under the 44 px comfortable touch target. The component adds no coarse-pointer expansion layer itself, so rely on the wrapping label for tap area, and give adjacent labels vertical breathing room in dense lists.

Group checkboxes under a visible question. Each item needs its own label; a parent or select-all checkbox needs text that explains what it covers, because the indeterminate mark alone carries no meaning out of context.

Colors come from --hui-* theme tokens, so hover, checked, danger, and disabled surfaces adapt to dark mode automatically. Layout uses logical properties, so nothing needs mirroring for right-to-left locales; keep custom indentation symmetric with logical utilities such as ms-4.

Long labels wrap inside the Label; they never truncate or push the indicator out of line. For multi-line content, use the card layout shown in Card style instead of cramming prose into a single label line.

Installation

npx honestui@latest add checkbox

Usage

import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";
<Checkbox />

A bare <Checkbox /> has no accessible name. Wrap it in a Label, or pass aria-label when no visible label fits:

<Label>
  <Checkbox defaultChecked />
  Email me about new comments
</Label>

Use controlled state (checked + onCheckedChange) only when the application owns the value; defaultChecked covers most forms.

Don't do this

Choosing one option from many

// Bad
<Label><Checkbox value="sm" /> Small</Label>
<Label><Checkbox value="md" /> Medium</Label>
<Label><Checkbox value="lg" /> Large</Label>
// Good
<RadioGroup defaultValue="md">
  <Label><Radio value="sm" /> Small</Label>
  <Label><Radio value="md" /> Medium</Label>
  <Label><Radio value="lg" /> Large</Label>
</RadioGroup>

Checkboxes promise independence: people expect to tick two sizes or none at all. When options are mutually exclusive, checkboxes invite invalid combinations and hide the constraint until validation fails. A radio group makes "exactly one" part of the interface instead of an error message.

Label text that isn't connected

// Bad
<div className="flex items-center gap-2">
  <Checkbox />
  <span>Email me about new comments</span>
</div>
// Good
<Label>
  <Checkbox />
  Email me about new comments
</Label>

Unassociated text is decoration as far as assistive technology is concerned: the checkbox is announced as an unnamed "checkbox", and tapping the words does nothing. Wrapping both in a Label names the control and turns the entire phrase into the click and touch target.

Controlled state without a handler

// Bad
<Checkbox checked={subscribed} />

// Good
<Checkbox
  checked={subscribed}
  onCheckedChange={(next) => setSubscribed(next)}
/>

// Good, if you don't need app state
<Checkbox defaultChecked={subscribed} />

Passing checked without onCheckedChange creates a control that ignores every click while looking broken to the person using it. If React state is not required, let the checkbox own its state with defaultChecked and read the result from form data.

Examples

Examples cover disabled, described, card-style, and form-connected checkboxes, followed by group patterns including parent and nested-parent selection.

For accessible labeling and validation, use Field to connect a checkbox with its label, description, and error. See the Field examples.

Disabled

Both unchecked and pre-checked items stay readable but reject interaction.

checkbox-disabled

With Description

checkbox-with-description

Card Style

The whole card is one Label, so the description extends the tap target too.

checkbox-card

Form Integration

Field provides accessible labeling and validation structure for form controls. Use it with Form to submit values.

checkbox-form

Checkbox Group

Checkbox Group is included in the same component file. It manages a single string[] value across its items and lays them out vertically by default; orientation="horizontal" wraps items in a row. Pass allValues to enable a parent checkbox that selects and clears every item, with the indeterminate mark appearing automatically while only some children are selected.

Basic Group

checkbox-group-demo

With Disabled Item

checkbox-group-disabled

Parent Checkbox

The parent selects all children and reports partial selection with the indeterminate dash.

checkbox-group-parent

Nested Parent Checkbox

Parent groups can contain child groups for hierarchical permissions.

checkbox-group-nested-parent

Group Form Integration

checkbox-group-form

API reference

Checkbox and CheckboxGroup forward their matching Base UI props and add Honest UI styling:

ComponentPropValuesDefault
Checkboxsizelarge, smalllarge
CheckboxGrouporientationvertical, horizontalvertical

On Checkbox: use checked/onCheckedChange or defaultChecked for state, indeterminate for the mixed visual, parent for select-all behavior inside a group, and name/value/uncheckedValue for form submission. An unchecked checkbox submits nothing unless you set uncheckedValue. disabled removes the item from interaction entirely; readOnly blocks changes while keeping the value submittable.

On CheckboxGroup: value/onValueChange hold the selected id array, defaultValue seeds it uncontrolled, and allValues defines the full set used by parent checkboxes.

See the Base UI Checkbox API and Checkbox Group API.