Skip to the content

ComponentsForm

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"

Usage

CheckboxGroup holds the value, an array of the checked items' values; CheckboxGroupItem is one checkbox of it, and CheckboxGroupParent checks or clears them all.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

Keyboard
KeyBehaviour
TabMoves to the next box in the group, then out of it.
SpaceToggles the focused item.
SpaceOn 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.

CheckboxGroup props
PropTypeDefaultDescription
allValuesstring[]Every item's value, for a parent checkbox drawn on the server before the items are known.
defaultValuestring[][]The checked items' values at the start, when it controls itself.
disabledbooleanfalseDisables every checkbox in the group.
formstringThe id of the form the value belongs to, for a control placed outside it.
namestringThe 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"verticalvertical (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.
valuestring[]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

CheckboxGroupItem props
PropTypeDefaultDescription
valuerequiredstringThe value the group's array holds while this box is checked.
asChildboolean
descriptionReactNodeA line of help under the label, read as the box's description.
iconReactNodeThe mark shown when checked, in place of the check. Sized with the box.
indeterminateIconReactNodeThe mark shown when mixed, in place of the dash. Sized with the box.
labelReactNodeThe visible name, drawn as a label beside the box. Leave it out to label the box yourself.
requiredboolean
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

CheckboxGroupParent props
PropTypeDefaultDescription
asChildboolean
descriptionReactNodeA line of help under the label, read as the box's description.
iconReactNodeThe mark shown when checked, in place of the check. Sized with the box.
indeterminateIconReactNodeThe mark shown when mixed, in place of the dash. Sized with the box.
labelReactNodeThe visible name, drawn as a label beside the box. Leave it out to label the box yourself.
requiredboolean
size"default" | "sm" | "lg"A 14, 18 or 20 px box. Defaults to the provider's controlSize.
valuesstring[]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 and takes its tokens. Descriptions are --muted-foreground.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--muted-foregroundtext