# Checkbox

Lets people choose one or more options, or shows that a group is partly chosen.

- **Import:** `import { Checkbox } from "@booleanpress/ui/checkbox"`
- **Radix Checkbox:** <https://www.radix-ui.com/primitives/docs/components/checkbox>
- **APG Checkbox:** <https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/>
- **Page:** <https://ui.booleanpress.com/components/checkbox> · @booleanpress/ui 0.1.0

## Usage

Give every checkbox a visible name: a `Label` whose `htmlFor` is the checkbox's `id`. Clicking the label toggles it.

```tsx
import { Checkbox } from "@booleanpress/ui/checkbox"
import { Label } from "@booleanpress/ui/label"

export function Terms() {
  return (
    <div className="flex items-center gap-2">
      <Checkbox id="terms" />
      <Label htmlFor="terms">Accept the terms</Label>
    </div>
  )
}
```

It is uncontrolled with `defaultChecked`, or controlled with `checked` and `onCheckedChange`. `checked` is `true`, `false` or `"indeterminate"`; `onCheckedChange` receives the same three values. It also takes a button's attributes, such as `disabled`, and, inside a form, `name` and `value`.

## Examples

### Basic

Unchecked and checked, each named by its label.

```tsx
import { Checkbox } from "@booleanpress/ui/checkbox"
import { Label } from "@booleanpress/ui/label"

export default function CheckboxBasic() {
  return (
    <div className="flex flex-col gap-3">
      <div className="flex items-center gap-2">
        <Checkbox id="terms" />
        <Label htmlFor="terms">Accept the terms</Label>
      </div>
      <div className="flex items-center gap-2">
        <Checkbox id="news" defaultChecked />
        <Label htmlFor="news">Send me product news</Label>
      </div>
    </div>
  )
}
```

### Indeterminate

A box that chooses a whole group shows a dash while only some of the group is chosen.

```tsx
import { useState } from "react"
import { Checkbox } from "@booleanpress/ui/checkbox"
import { Label } from "@booleanpress/ui/label"

const EVENTS = [
  { id: "delivered", label: "Delivered" },
  { id: "bounced", label: "Bounced" },
  { id: "failed", label: "Failed" },
]

export default function CheckboxIndeterminate() {
  const [chosen, setChosen] = useState<string[]>(["bounced"])
  const all = chosen.length === EVENTS.length ? true : chosen.length > 0 ? "indeterminate" : false

  return (
    <fieldset className="flex flex-col gap-3">
      <legend className="sr-only">Notify me about</legend>
      <div className="flex items-center gap-2">
        <Checkbox
          id="events-all"
          checked={all}
          onCheckedChange={(checked) => setChosen(checked === true ? EVENTS.map((e) => e.id) : [])}
        />
        <Label htmlFor="events-all">All events</Label>
      </div>
      {EVENTS.map((event) => (
        <div key={event.id} className="ms-6 flex items-center gap-2">
          <Checkbox
            id={`events-${event.id}`}
            checked={chosen.includes(event.id)}
            onCheckedChange={(checked) =>
              setChosen((current) => (checked === true ? [...current, event.id] : current.filter((id) => id !== event.id)))
            }
          />
          <Label htmlFor={`events-${event.id}`}>{event.label}</Label>
        </div>
      ))}
    </fieldset>
  )
}
```

### Disabled

A disabled box keeps its state, ignores clicks and Space, and leaves the tab order.

```tsx
import { Checkbox } from "@booleanpress/ui/checkbox"
import { Label } from "@booleanpress/ui/label"

export default function CheckboxDisabled() {
  return (
    <div className="flex flex-col gap-3">
      <div className="flex items-center gap-2">
        <Checkbox id="backups" disabled />
        <Label htmlFor="backups">Daily backups</Label>
      </div>
      <div className="flex items-center gap-2">
        <Checkbox id="logging" disabled defaultChecked />
        <Label htmlFor="logging">Keep the email log</Label>
      </div>
    </div>
  )
}
```

### Invalid

`aria-invalid` draws the error border; `aria-describedby` reads the error message with the name.

```tsx
import { Checkbox } from "@booleanpress/ui/checkbox"
import { Label } from "@booleanpress/ui/label"

export default function CheckboxInvalid() {
  return (
    <div className="flex flex-col gap-1.5">
      <div className="flex items-center gap-2">
        <Checkbox id="consent" aria-invalid aria-describedby="consent-error" />
        <Label htmlFor="consent">I have permission to email these contacts</Label>
      </div>
      <p id="consent-error" className="ms-6 text-sm text-destructive">
        Confirm the permission to continue.
      </p>
    </div>
  )
}
```

## Accessibility

**Semantics.** A `button` with `role="checkbox"` and `aria-checked` (`true`, `false` or `mixed`). Inside a form, it also renders a hidden native input, so the value is submitted with the form.

**Labels.** Name every checkbox: a `Label` with `htmlFor`, or `aria-label` when no visible text fits. Put an error message in `aria-describedby`.

**Focus.** It is in the tab order. The focus ring shows on keyboard focus, not on click.

**Known limits.**

- The box is 16 px. Keep its label beside it, so the label adds to the target and the pair meets WCAG 2.5.8's 24 px.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Space | Toggles between checked and unchecked. From the indeterminate state, it becomes checked. |

## API

### Checkbox

Renders Radix Checkbox.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `checked` | `boolean \| "indeterminate"` |  | The state, when you control it. Pair it with `onCheckedChange`. |
| `defaultChecked` | `boolean \| "indeterminate"` |  | The state it starts in, when it controls itself. |
| `onCheckedChange` | `((checked: boolean \| "indeterminate") => void)` |  | Called with the new state when it is toggled. |
| `required` | `boolean` |  | The form cannot be submitted until it is checked. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="checkbox"` (Checkbox).

## Theming

The unchecked edge is `--control`, at least 3:1 against the surface (WCAG 1.4.11); checked and mixed fill with `--primary` and `--primary-foreground`; focus is `--ring` at 50 %; invalid is `--destructive`.

| Token | Used for |
| --- | --- |
| `--control` | border |
| `--destructive` | border, focus ring |
| `--input` | border, background |
| `--primary` | border, background |
| `--primary-foreground` | text |
| `--ring` | border, focus ring |
