Gesso
Forms

Checkbox

A 20px check control on the primary, with a 44px coarse hit area.

Checkbox is a yes or no answer that a form submits, like opting into an email or accepting the terms, and several in a list each stand on their own. The 20px box fills with bg-tone, the primary unless you set a tone, and draws a bold check when it is checked. It has no label of its own, so pair it with Field and do not ship a box without one.

Usage

import { Checkbox } from '@/components/ui/checkbox';
import { Field, FieldLabel, FieldRow } from '@/components/ui/field';

<Field>
  <FieldRow>
    <Checkbox
      id="weekly-summary"
      checked={weekly}
      onCheckedChange={(checked) => setWeekly(checked === true)}
    />
    <FieldLabel htmlFor="weekly-summary">Email me a weekly summary</FieldLabel>
  </FieldRow>
</Field>

The id on the box and the htmlFor on the label tie them together, so a click on the label checks the box and the label becomes its name.

Holding the checked state

Pass checked and onCheckedChange to hold the state yourself, as in the usage above, or defaultChecked to let the box keep it. onCheckedChange hands you true, false, or "indeterminate", so compare with === true when you store a boolean. The box sets data-state to checked, unchecked, or indeterminate for your own styles.

Radix also takes checked="indeterminate", for a parent box whose children are only partly checked, and reports it to assistive tech as mixed. A mixed box fills like a checked one and draws a dash, and a click on it checks it. Below, the parent box follows its three children.

A description under the label

FieldRow align="start" with a FieldContent stacks a description under the label, and the box stays on the first line, because the label's line is 20px tall like the box. Field does not connect the description to the box, so give the description an id and point the box's aria-describedby at it, and a screen reader reads it after the name.

Email me when someone mentions me in an issue or a comment.

A digest of what changed in your projects, every Monday.

In a form

Inside a <form>, Checkbox renders a hidden native checkbox beside the box and keeps it in step, so the form sends value under name while the box is checked and nothing while it is not. value is "on" unless you set one, and boxes that share a name send a list. required goes on the hidden input too, so the browser holds the submit until the box is checked, and a form reset puts the box back where it started. Outside a form there is no hidden input, unless you pass form with the id of a form elsewhere on the page.

To show that a box needs attention, set aria-invalid and its border turns border-destructive. Put the reason in a FieldError, which has role="alert" so it is read out as it appears, and point aria-describedby at it, as the terms box at the top of the page does. Save and Reset below show what the form sends.

Save to see what the form sends.

Tone

A checked box fills with the primary by default. Set tone to danger, success, warning, info, or neutral to change the hue, or inherit to take the tone of the surrounding scope. An invalid box still turns its border to the danger color, whatever its tone.

Accessibility

  • It renders a <button type="button" role="checkbox"> with aria-checked, so assistive tech announces a checkbox and whether it is checked, or mixed when it is indeterminate.
  • Space toggles it. Radix stops Enter, so Enter does not.
  • The name comes from the FieldLabel whose htmlFor matches the box's id. A box with no visible label needs an aria-label.
  • required sets aria-required, and aria-invalid reports the box as invalid as well as painting its border. Field does not wire a description or an error to the box, so set aria-describedby yourself.
  • Focus shows a 2px ring on :focus-visible only, offset from the box, in the contrast ink rather than the brand color.
  • In forced colors the border is ButtonText, a checked box fills with Highlight under a HighlightText check, and a disabled one turns GrayText. A box that is disabled and checked or mixed fills with GrayText under a Canvas check or dash. Focus keeps a transparent outline that the system paints, so it stays visible in Windows high contrast.
  • The box is 20px. On a coarse pointer, such as a touch screen, an invisible 44px square centered on it takes the tap too, and the label is a target as well. With a mouse the extra area is off.
  • disabled sets the native disabled attribute, so the box leaves the tab order, is reported as unavailable, and the FieldLabel beside it dims.
  • The check scales in over 120ms. Under reduced motion it appears at once, and the fill changes without a transition.

When to use something else

  • Switch for a setting that takes effect the moment it flips, as in a settings list, with no submit.
  • Radio group when the options exclude each other, so exactly one of a few is chosen, and Select when that one comes from a long list.
  • Toggle for a toolbar button that stays pressed, like bold, which has no label beside it and no form value.

Install

shadcn add @gesso/checkbox

On this page