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 arole="checkbox"button witharia-checked; the parent readsmixedwhile some items are checked and lists its items inaria-controls, as in the APG mixed checkbox. Inside a form, each box also renders a hidden native input. - Labels
- Name the group with
aria-labeloraria-labelledby, or put it in aFieldSetwith aFieldLegend. Each box is named by itslabel(or your ownLabel); adescriptionis 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
allValuesor the parent hasvalues. - The group's
aria-describedbyis read when focus enters the group in some screen readers only; for an error on every box, also mark the boxes witharia-describedby.
- The parent's first render on the server does not know the items unless the group has
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 |
|---|---|---|---|
valuerequired | 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 and takes its tokens. Descriptions are --muted-foreground.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--muted-foreground | text |