# Checkbox

A choice that takes effect when the form is submitted.

- Group: Forms
- Import: `import { Checkbox } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/checkbox/
- Related: switch, radio-group

## When to reach for it

A setting that applies immediately is a Switch.

## Anatomy

- **Box** (required) — An 18px <button role="checkbox"> on --radius-xs. It fills with --accent for BOTH checked and indeterminate, so the fill says “not off” rather than “on”.
- **Tick** — The check glyph, aria-hidden — Radix shows the indicator, the state is carried by the role.
- **Dash** — The minus that replaces the tick, chosen from the state the box is actually in — controlled or not, so defaultChecked="indeterminate" draws the dash it promised rather than a tick.
- **Label** — Not rendered here. Unlike RadioGroupItem, nothing wraps the box in a <label>, so the words beside it and the click target they give it are the call site’s job.

## Best practices

### Do

- Wrap it and its words in a <label>, or put it in a Field: the control renders no label of its own, so a bare Checkbox has no accessible name and an 18px box is the entire click target.
- Hold an indeterminate box on controlled state. It is a report about OTHER rows, and clicking it hands you true — a “select all” header that keeps its own answer stops describing the list underneath it on the first click.
- Default the value on the server. An unticked box sends no entry at all in a form submission, so the field a reader deliberately cleared and a field that was never rendered arrive identically as undefined.

### Don’t

- checked without onCheckedChange gives a box that never moves: Radix treats the prop as the source of truth, so the reader clicks a control that is neither broken nor working and gets no feedback either way.
- There is no readOnly on a Radix checkbox. disabled is the only lock and it drops the box out of the tab order and out of the form, so a value that must be shown but not changed is better drawn as text.
- Do not put the indeterminate state on a leaf. It means “some of the things under this one”, so a box with nothing under it that draws a dash is reporting a state its own value cannot hold.

## Accessibility

- Supports the indeterminate state, which is what a “select all” header needs when only some rows are selected.

## Keyboard

- Space — Toggles it.

## Checkbox

A choice that takes effect when the form is submitted. Supports the indeterminate state (`checked="indeterminate"`), which is what a "select all" header needs when only some rows are selected — a plain unchecked box there tells the reader the opposite of the truth. Controlled or not: the glyph follows the state the box is actually in, so an uncontrolled `defaultChecked="indeterminate"` draws the dash rather than the tick that says the opposite. Pair with `Field`, or wrap it in a `<label>` at the call site so the words beside it are part of the click target.

Also accepts: `ComponentProps<typeof CheckboxPrimitive.Root>`.

## Example — default

```tsx
import { Checkbox } from '@misoto22/folio'

<div className="flex flex-col gap-3 text-sm text-(--ink-2)">
  <label className="flex cursor-pointer items-center gap-2.5">
    <Checkbox
      checked={some ? 'indeterminate' : all}
      onCheckedChange={(next) => setChecked(next === true ? JOBS : [])}
    />
    Select all
  </label>

  <div className="ms-6 flex flex-col gap-3 border-s border-(--rule) ps-4">
    {JOBS.map((job) => (
      <label key={job} className="flex cursor-pointer items-center gap-2.5">
        <Checkbox
          checked={checked.includes(job)}
          onCheckedChange={(next) =>
            setChecked((previous) =>
              next === true ? [...previous, job] : previous.filter((item) => item !== job),
            )
          }
        />
        {job}
      </label>
    ))}
  </div>

  <label className="flex items-center gap-2.5 text-(--ink-3-aa)">
    <Checkbox disabled /> Requires admin
  </label>
</div>
```

## Example — with descriptions

```tsx
import { Checkbox, Text } from '@misoto22/folio'

<div className="flex max-w-sm flex-col gap-4">
  {OPTIONS.map((option) => (
    <label key={option.id} className="flex cursor-pointer items-start gap-3">
      <Checkbox defaultChecked={option.on} className="mt-0.5" />
      <span className="flex flex-col gap-0.5">
        <Text as="span" size="sm" tone="strong">
          {option.label}
        </Text>
        <Text as="span" size="xs" tone="muted">
          {option.detail}
        </Text>
      </span>
    </label>
  ))}
</div>
```

## Example — inside a form

```tsx
import { Button, Checkbox, Text } from '@misoto22/folio'

<form
  className="flex max-w-sm flex-col gap-3"
  onSubmit={(event) => {
    event.preventDefault()
    const keys = [...new FormData(event.currentTarget).keys()]
    setSent(keys.length > 0 ? keys.join(', ') : 'nothing at all')
  }}
>
  <label className="flex cursor-pointer items-center gap-2.5 text-sm text-(--ink-2)">
    <Checkbox name="backups" value="nightly" defaultChecked /> Nightly backups
  </label>
  <label className="flex cursor-pointer items-center gap-2.5 text-sm text-(--ink-2)">
    <Checkbox name="telemetry" value="anonymous" /> Share anonymous usage data
  </label>
  <Button type="submit" variant="secondary" className="self-start">
    Save
  </Button>
  {sent !== undefined && (
    <Text size="sm" tone="muted">
      The form sent: {sent}
    </Text>
  )}
</form>
```
