# Checkbox group

Checkboxes that share one array value, with a parent checkbox that checks or clears them all.

- **Import:** `import { CheckboxGroup, CheckboxGroupItem, CheckboxGroupParent } from "@booleanpress/ui/checkbox-group"`
- **Radix Checkbox:** <https://www.radix-ui.com/primitives/docs/components/checkbox>
- **APG Checkbox (mixed state):** <https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/examples/checkbox-mixed/>
- **Page:** <https://ui.booleanpress.com/components/checkbox-group> · @booleanpress/ui 0.2.0

## Usage

`CheckboxGroup` holds the value, an array of the checked items' `value`s; `CheckboxGroupItem` is one [checkbox](/components/checkbox) of it, and `CheckboxGroupParent` checks or clears them all.

```tsx
import { CheckboxGroup, CheckboxGroupItem, CheckboxGroupParent } from "@booleanpress/ui/checkbox-group"

export function Events() {
  return (
    <CheckboxGroup defaultValue={["delivered"]} aria-label="Log these events">
      <CheckboxGroupParent />
      <CheckboxGroupItem value="delivered" label="Delivered" />
      <CheckboxGroupItem value="bounced" label="Bounced" />
    </CheckboxGroup>
  )
}
```

It is uncontrolled with `defaultValue`, or controlled with `value` and `onValueChange`. An item's `label` draws a label beside its box, and `description` a line under it, read as the box's description; leave `label` out to label the box yourself with a `Label`. `CheckboxGroupParent` is checked when every item is, mixed when some are, and is labelled "Select all" (the provider string `selectAll`) unless you give it a `label`. Give it `values` to control part of the group, for nested groups. It leaves disabled items alone: its state counts only the items it can change, and it is disabled itself when all of them are. The parent learns the items once they mount; pass `allValues` to the group when it renders on the server, so the parent's first state is right. `orientation="horizontal"` puts the items in a wrapping row. `size` and `variant` reach every box and follow the provider; `disabled` disables them all, and `aria-invalid` marks them all invalid. With `name`, each checked item submits its value in a form, and a form reset brings back the first value.

## Examples

### Basic

Four labelled boxes sharing one value; one is checked.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupBasic() {
  return (
    <CheckboxGroup defaultValue={["delivered"]} aria-label="Log these events">
      <CheckboxGroupItem value="delivered" label="Delivered" />
      <CheckboxGroupItem value="opened" label="Opened" />
      <CheckboxGroupItem value="clicked" label="Clicked" />
      <CheckboxGroupItem value="bounced" label="Bounced" />
    </CheckboxGroup>
  )
}
```

### Controlled

`value` and `onValueChange`, the array shown above, in a horizontal row.

```tsx
import { useState } from "react"
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupControlled() {
  const [channels, setChannels] = useState(["email"])

  return (
    <div className="flex flex-col items-center gap-4">
      <p className="font-mono text-sm/normal text-muted-foreground">onValueChange: {JSON.stringify(channels)}</p>
      <CheckboxGroup value={channels} onValueChange={setChannels} orientation="horizontal" aria-label="Alert channels">
        <CheckboxGroupItem value="email" label="Email" />
        <CheckboxGroupItem value="sms" label="SMS" />
        <CheckboxGroupItem value="slack" label="Slack" />
        <CheckboxGroupItem value="webhook" label="Webhook" />
      </CheckboxGroup>
    </div>
  )
}
```

### Dynamic

Items made from an array, each with a description under its label.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

const SCOPES = [
  { value: "mail.send", label: "Send email", description: "Send through every connected mailer." },
  { value: "logs.read", label: "Read delivery logs", description: "See each message's status and events." },
  { value: "contacts.write", label: "Edit contacts", description: "Add, change and remove subscribers." },
  { value: "keys.manage", label: "Manage API keys", description: "Create and revoke other keys." },
]

export default function CheckboxGroupDynamic() {
  return (
    <CheckboxGroup defaultValue={["mail.send", "logs.read"]} aria-label="API key scopes" className="max-w-sm">
      {SCOPES.map((scope) => (
        <CheckboxGroupItem key={scope.value} value={scope.value} label={scope.label} description={scope.description} />
      ))}
    </CheckboxGroup>
  )
}
```

### Select all

`CheckboxGroupParent` turns mixed while some items are checked; Space checks all, clears all, then brings the mix back.

```tsx
import { CheckboxGroup, CheckboxGroupItem, CheckboxGroupParent } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupSelectAll() {
  return (
    <CheckboxGroup defaultValue={["orders"]} aria-label="Mailing lists">
      <CheckboxGroupParent />
      <div className="flex flex-col gap-3 ps-6.5">
        <CheckboxGroupItem value="orders" label="Order receipts" />
        <CheckboxGroupItem value="news" label="Product news" />
        <CheckboxGroupItem value="digest" label="Weekly digest" />
      </div>
    </CheckboxGroup>
  )
}
```

### Nested group

A parent per section, each given its section's `values`, under a parent for every permission.

```tsx
import { CheckboxGroup, CheckboxGroupItem, CheckboxGroupParent } from "@booleanpress/ui/checkbox-group"
import { Separator } from "@booleanpress/ui/separator"

const SECTIONS = [
  { title: "Tickets", description: "Answer and route customer tickets", items: [["tickets.reply", "Reply to tickets"], ["tickets.assign", "Assign tickets"], ["tickets.delete", "Delete tickets"]] },
  { title: "Customers", description: "Manage customer records", items: [["customers.view", "View customers"], ["customers.merge", "Merge customers"]] },
  { title: "Billing", description: "See invoices and payment methods", items: [["billing.invoices", "View invoices"], ["billing.methods", "Change payment methods"]] },
]

export default function CheckboxGroupNested() {
  return (
    <CheckboxGroup defaultValue={["tickets.reply", "tickets.assign", "customers.view"]} aria-label="Agent permissions" className="w-full max-w-md">
      <CheckboxGroupParent label="All permissions" />
      {SECTIONS.map((section, index) => (
        <div key={section.title} className="flex flex-col gap-3 ps-6.5">
          {index > 0 ? <Separator /> : null}
          <CheckboxGroupParent label={section.title} description={section.description} values={section.items.map(([value]) => value)} />
          <div className="flex flex-col gap-3 ps-6.5">
            {section.items.map(([value, label]) => (
              <CheckboxGroupItem key={value} value={value} label={label} />
            ))}
          </div>
        </div>
      ))}
    </CheckboxGroup>
  )
}
```

### Horizontal

`orientation="horizontal"` lays the days out in a wrapping row.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupHorizontal() {
  return (
    <CheckboxGroup orientation="horizontal" defaultValue={["mon", "tue", "wed", "thu", "fri"]} aria-label="Sending days">
      {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day) => (
        <CheckboxGroupItem key={day} value={day.toLowerCase()} label={day} />
      ))}
    </CheckboxGroup>
  )
}
```

### Disabled

`disabled` on the group disables every box; on an item, that box alone.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupDisabled() {
  return (
    <div className="flex flex-col gap-8">
      <CheckboxGroup disabled defaultValue={["smtp"]} aria-label="Fallback mailers, locked">
        <CheckboxGroupItem value="smtp" label="SMTP" />
        <CheckboxGroupItem value="ses" label="Amazon SES" />
      </CheckboxGroup>
      <CheckboxGroup defaultValue={["ses"]} aria-label="Fallback mailers">
        <CheckboxGroupItem value="smtp" label="SMTP" />
        <CheckboxGroupItem value="ses" label="Amazon SES" />
        <CheckboxGroupItem value="postmark" label="Postmark (no API key yet)" disabled />
      </CheckboxGroup>
    </div>
  )
}
```

### Invalid

`aria-invalid` gives every box the error edge; `aria-describedby` on the group points at the message.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupInvalid() {
  return (
    <div className="flex flex-col gap-3">
      <span id="consent-label" className="text-sm/normal font-medium text-foreground">
        Before you import
      </span>
      <CheckboxGroup aria-labelledby="consent-label" aria-describedby="consent-error" aria-invalid>
        <CheckboxGroupItem value="opt-in" label="Every contact opted in" />
        <CheckboxGroupItem value="terms" label="I accept the import terms" />
      </CheckboxGroup>
      <p id="consent-error" className="text-sm/normal text-destructive-strong">
        Confirm both to import the list.
      </p>
    </div>
  )
}
```

### Sizes

`size` on the group: `sm`, `default` and `lg` side by side.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

const SIZES = [
  { size: "sm", label: "Small" },
  { size: "default", label: "Default" },
  { size: "lg", label: "Large" },
] as const

export default function CheckboxGroupSizes() {
  return (
    <div className="flex flex-wrap items-start gap-8">
      {SIZES.map(({ size, label }) => (
        <CheckboxGroup key={size} size={size} defaultValue={["delivered"]} aria-label={`${label} checkboxes: log these events`}>
          <CheckboxGroupItem value="delivered" label="Delivered" />
          <CheckboxGroupItem value="opened" label="Opened" />
          <CheckboxGroupItem value="clicked" label="Clicked" />
        </CheckboxGroup>
      ))}
    </div>
  )
}
```

### Filled

`variant="filled"` on the group fills every checkbox.

```tsx
import { CheckboxGroup, CheckboxGroupItem } from "@booleanpress/ui/checkbox-group"

export default function CheckboxGroupFilled() {
  return (
    <CheckboxGroup variant="filled" defaultValue={["delivered"]} aria-label="Log these events">
      <CheckboxGroupItem value="delivered" label="Delivered" />
      <CheckboxGroupItem value="opened" label="Opened" />
      <CheckboxGroupItem value="clicked" label="Clicked" />
      <CheckboxGroupItem value="bounced" label="Bounced" />
    </CheckboxGroup>
  )
}
```

## Accessibility

**Semantics.** The group is a `role="group"`. Each item is a `role="checkbox"` button with `aria-checked`; the parent reads `mixed` while some items are checked and lists its items in `aria-controls`, as in the APG mixed checkbox. Inside a form, each box also renders a hidden native input.

**Labels.** Name the group with `aria-label` or `aria-labelledby`, or put it in a `FieldSet` with a `FieldLegend`. Each box is named by its `label` (or your own `Label`); a `description` is its accessible description. The parent is "Select all" by default.

**Focus.** Every box is a tab stop, in order. The focus outline shows on keyboard focus, not on click.

**Known limits.**

- The parent's first render on the server does not know the items unless the group has `allValues` or the parent has `values`.
- The group's `aria-describedby` is read when focus enters the group in some screen readers only; for an error on every box, also mark the boxes with `aria-describedby`.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves to the next box in the group, then out of it. |
| Space | Toggles the focused item. |
| Space | On the parent: from mixed checks every item, from checked clears them, from cleared brings back the last mix (or checks all). |

## API

### CheckboxGroup

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `allValues` | `string[]` |  | Every item's value, for a parent checkbox drawn on the server before the items are known. |
| `defaultValue` | `string[]` | `[]` | The checked items' values at the start, when it controls itself. |
| `disabled` | `boolean` | `false` | Disables every checkbox in the group. |
| `form` | `string` |  | The id of the form the value belongs to, for a control placed outside it. |
| `name` | `string` |  | The form field name of every checkbox; each checked item submits its value under it. |
| `onValueChange` | `((value: string[]) => void)` |  | Called with the new array when an item or a parent checkbox is toggled. |
| `orientation` | `"horizontal" \| "vertical"` | `vertical` | `vertical` (default) stacks the items; `horizontal` puts them in a wrapping row. |
| `size` | `"default" \| "sm" \| "lg"` |  | The size of every checkbox in the group. Defaults to the provider's `controlSize`. |
| `value` | `string[]` |  | The checked items' values, when you control them. |
| `variant` | `"default" \| "filled"` |  | The look of every checkbox in the group. Defaults to the provider's `fieldVariant`. |

### CheckboxGroupItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The value the group's array holds while this box is checked. |
| `asChild` | `boolean` |  |  |
| `description` | `ReactNode` |  | A line of help under the label, read as the box's description. |
| `icon` | `ReactNode` |  | The mark shown when checked, in place of the check. Sized with the box. |
| `indeterminateIcon` | `ReactNode` |  | The mark shown when mixed, in place of the dash. Sized with the box. |
| `label` | `ReactNode` |  | The visible name, drawn as a label beside the box. Leave it out to label the box yourself. |
| `required` | `boolean` |  |  |
| `size` | `"default" \| "sm" \| "lg"` |  | A 14, 18 or 20 px box. Defaults to the provider's `controlSize`. |
| `variant` | `"default" \| "filled"` |  | `filled` fills the unchecked box grey. Defaults to the provider's `fieldVariant`. |

### CheckboxGroupParent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `description` | `ReactNode` |  | A line of help under the label, read as the box's description. |
| `icon` | `ReactNode` |  | The mark shown when checked, in place of the check. Sized with the box. |
| `indeterminateIcon` | `ReactNode` |  | The mark shown when mixed, in place of the dash. Sized with the box. |
| `label` | `ReactNode` |  | The visible name, drawn as a label beside the box. Leave it out to label the box yourself. |
| `required` | `boolean` |  |  |
| `size` | `"default" \| "sm" \| "lg"` |  | A 14, 18 or 20 px box. Defaults to the provider's `controlSize`. |
| `values` | `string[]` |  | The values it controls: a nested group's items. Every item of the group by default. |
| `variant` | `"default" \| "filled"` |  | `filled` fills the unchecked box grey. Defaults to the provider's `fieldVariant`. |

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

**Data attributes:** `data-slot="checkbox-group"` (CheckboxGroup), `data-slot="checkbox-group-parent"` (CheckboxGroupParent), and `data-orientation`, `data-disabled`, `data-invalid`.

**Provider strings:** `selectAll` (`BooleanUIProvider`'s `strings`).

## Theming

Each box is a [checkbox](/components/checkbox) and takes its tokens. Descriptions are `--muted-foreground`.

| Token | Used for |
| --- | --- |
| `--muted-foreground` | text |
