Checkbox Group
Shares selection state across a set of checkboxes.
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldItem, FieldLabel } from "@/components/ui/field"
export default function CheckboxGroupDemo() {
return (
<Field>
<FieldLabel nativeLabel={false} render={<div />}>
Apples
</FieldLabel>
<CheckboxGroup defaultValue={["fuji"]}>
{[
["fuji", "Fuji"],
["gala", "Gala"],
["granny-smith", "Granny Smith"],
].map(([value, label]) => (
<FieldItem key={value}>
<FieldLabel>
<Checkbox value={value} />
{label}
</FieldLabel>
</FieldItem>
))}
</CheckboxGroup>
</Field>
)
}Installation
Install the following dependencies:
pnpm add @base-ui/react @stylexjs/stylexnpm install @base-ui/react @stylexjs/stylexyarn add @base-ui/react @stylexjs/stylexbun add @base-ui/react @stylexjs/stylexSet up Installation, including state-styles.ts. Install Checkbox for the choices and Field for labels and validation.
Copy and paste the following code into your project.
"use client"
import type { CSSProperties } from "react"
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { stateStyles } from "@/lib/state-styles"
import { space } from "@/theme/tokens.stylex"
const styles = stylex.create({ root: { display: "flex", flexDirection: "column", gap: space.s3 } })
const checkboxGroupStyles = { root: styles.root }
function CheckboxGroup({
xstyle,
className,
style,
...props
}: Omit<CheckboxGroupPrimitive.Props, "className" | "style"> & {
xstyle?: StyleXStyles
className?: string
style?: CSSProperties
}) {
return (
<CheckboxGroupPrimitive
data-slot="checkbox-group"
{...props}
{...stateStyles(() => [styles.root, xstyle], { className, style })}
/>
)
}
export { CheckboxGroup, checkboxGroupStyles }Update the import paths to match your project setup.
Usage
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldItem, FieldLabel } from "@/components/ui/field"<Field name="fruit">
<FieldLabel nativeLabel={false} render={<div />}>
Fruit
</FieldLabel>
<CheckboxGroup defaultValue={["apple"]}>
<FieldItem>
<FieldLabel>
<Checkbox value="apple" />
Apple
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Checkbox value="pear" />
Pear
</FieldLabel>
</FieldItem>
</CheckboxGroup>
</Field>Set each checkbox's value and pass selected values to the group. Put name on Field for form submission.
Labeling a checkbox group
Use FieldLabel for the group caption and FieldItem to associate each option with its own label.
Select the items you want to show on the desktop.
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldDescription, FieldItem, FieldLabel } from "@/components/ui/field"
const items = [
{ value: "hard-disks", label: "Hard disks" },
{ value: "external-disks", label: "External disks" },
{ value: "cds-dvds", label: "CDs, DVDs, and iPods" },
{ value: "connected-servers", label: "Connected servers" },
]
export default function CheckboxGroupExample() {
return (
<Field name="desktop-items">
<FieldLabel nativeLabel={false} render={<div />}>
Show these items on the desktop:
</FieldLabel>
<FieldDescription>Select the items you want to show on the desktop.</FieldDescription>
<CheckboxGroup defaultValue={["hard-disks", "external-disks"]}>
{items.map(({ value, label }) => (
<FieldItem key={value}>
<FieldLabel>
<Checkbox value={value} />
{label}
</FieldLabel>
</FieldItem>
))}
</CheckboxGroup>
</Field>
)
}Rendering as a native button
Use nativeButton and render={<button />} when placing the label beside its checkbox.
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldItem, FieldLabel } from "@/components/ui/field"
import { HStack } from "@/components/ui/stack"
export default function CheckboxGroupNativeButton() {
return (
<Field>
<FieldLabel nativeLabel={false} render={<div />}>
Allowed network protocols
</FieldLabel>
<CheckboxGroup defaultValue={["https"]}>
{["http", "https", "ssh"].map((value) => (
<FieldItem key={value}>
<HStack gap={2}>
<Checkbox
value={value}
nativeButton
render={<button aria-label={value.toUpperCase()} />}
/>
<FieldLabel>{value.toUpperCase()}</FieldLabel>
</HStack>
</FieldItem>
))}
</CheckboxGroup>
</Field>
)
}Form integration
Validate the selected array with Field and display its message with FieldError.
import { useState } from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldDescription, FieldError, FieldItem, FieldLabel } from "@/components/ui/field"
import { Form } from "@/components/ui/form"
import { Button } from "@/components/ui/button"
export default function CheckboxGroupForm() {
const [submitted, setSubmitted] = useState<string[] | null>(null)
return (
<Form onFormSubmit={(values) => setSubmitted(values.protocols as string[])}>
<Field
name="protocols"
validate={(value) =>
Array.isArray(value) && value.length > 0 ? null : "Select at least one protocol."
}
>
<FieldLabel nativeLabel={false} render={<div />}>
Allowed network protocols
</FieldLabel>
<FieldDescription>Select one or more protocols.</FieldDescription>
<CheckboxGroup>
{["http", "https", "ssh"].map((value) => (
<FieldItem key={value}>
<FieldLabel>
<Checkbox value={value} />
{value.toUpperCase()}
</FieldLabel>
</FieldItem>
))}
</CheckboxGroup>
<FieldError />
</Field>
<Button type="submit">Save protocols</Button>
{submitted && <output>Saved: {submitted.join(", ")}</output>}
</Form>
)
}Parent checkbox
Use parent on a checkbox and pass allValues, value, and onValueChange to the group.
import * as stylex from "@stylexjs/stylex"
import { VStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"
import { useState } from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldItem, FieldLabel } from "@/components/ui/field"
const styles = stylex.create({ children: { marginInlineStart: space.s4 } })
const values = ["fuji", "gala", "granny-smith"]
export default function CheckboxGroupParent() {
const [value, setValue] = useState(["fuji"])
return (
<Field>
<CheckboxGroup aria-label="Apples" allValues={values} value={value} onValueChange={setValue}>
<FieldItem>
<FieldLabel>
<Checkbox parent />
Apples
</FieldLabel>
</FieldItem>
<VStack gap={3} xstyle={styles.children}>
{[
["fuji", "Fuji"],
["gala", "Gala"],
["granny-smith", "Granny Smith"],
].map(([value, label]) => (
<FieldItem key={value}>
<FieldLabel>
<Checkbox value={value} />
{label}
</FieldLabel>
</FieldItem>
))}
</VStack>
</CheckboxGroup>
</Field>
)
}Nested parent checkbox
Keep the nested group's values in the outer selection to update both parents together.
import { useId, useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldItem, FieldLabel } from "@/components/ui/field"
import { VStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"
const management = ["create-user", "edit-user", "delete-user", "assign-roles"]
const permissions = ["view-dashboard", "access-reports", ...management]
const labels: Record<string, string> = {
"view-dashboard": "View Dashboard",
"access-reports": "Access Reports",
"create-user": "Create User",
"edit-user": "Edit User",
"delete-user": "Delete User",
"assign-roles": "Assign Roles",
}
const styles = stylex.create({ nested: { marginInlineStart: space.s4 } })
export default function CheckboxGroupNested() {
const controlsId = useId()
const [value, setValue] = useState<string[]>([])
return (
<Field>
<CheckboxGroup
aria-label="User Permissions"
allValues={permissions}
value={value}
onValueChange={setValue}
>
<FieldItem>
<FieldLabel>
<Checkbox parent render={(props) => <span {...props} aria-controls={controlsId} />} />
User Permissions
</FieldLabel>
</FieldItem>
<VStack id={controlsId} gap={3} xstyle={styles.nested}>
{["view-dashboard", "access-reports"].map((permission) => (
<FieldItem key={permission}>
<FieldLabel>
<Checkbox value={permission} />
{labels[permission]}
</FieldLabel>
</FieldItem>
))}
<CheckboxGroup
aria-label="Manage Users"
allValues={management}
value={value.filter((v) => management.includes(v))}
onValueChange={(next) =>
setValue((current) => [...current.filter((v) => !management.includes(v)), ...next])
}
>
<FieldItem>
<FieldLabel>
<Checkbox parent />
Manage Users
</FieldLabel>
</FieldItem>
<VStack gap={3} xstyle={styles.nested}>
{management.map((permission) => (
<FieldItem key={permission}>
<FieldLabel>
<Checkbox value={permission} />
{labels[permission]}
</FieldLabel>
</FieldItem>
))}
</VStack>
</CheckboxGroup>
</VStack>
</CheckboxGroup>
</Field>
)
}Controlled
Use value and onValueChange for controlled selection; use FieldItem disabled for an unavailable option.
Selected: email
import { useState } from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldDescription, FieldItem, FieldLabel } from "@/components/ui/field"
import { HStack } from "@/components/ui/stack"
export default function CheckboxGroupControlled() {
const [value, setValue] = useState(["email"])
return (
<Field>
<FieldLabel nativeLabel={false} render={<div />}>
Notifications
</FieldLabel>
<CheckboxGroup value={value} onValueChange={setValue}>
{["email", "sms", "push"].map((option) => (
<FieldItem key={option} disabled={option === "push"}>
<HStack gap={2}>
<Checkbox value={option} nativeButton render={<button aria-label={option} />} />
<FieldLabel>{option}</FieldLabel>
</HStack>
</FieldItem>
))}
</CheckboxGroup>
<FieldDescription>Selected: {value.join(", ") || "none"}</FieldDescription>
</Field>
)
}Disabled and invalid
Set disabled or invalid on Field to style every checkbox in the group.
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldError, FieldItem, FieldLabel } from "@/components/ui/field"
import { HStack, VStack } from "@/components/ui/stack"
export default function CheckboxGroupDisabled() {
return (
<VStack gap={6}>
{(["disabled", "invalid", "disabled-invalid"] as const).map((state) => (
<Field
key={state}
disabled={state.includes("disabled")}
invalid={state.includes("invalid")}
>
<FieldLabel nativeLabel={false} render={<div />}>
{state}
</FieldLabel>
<CheckboxGroup defaultValue={["email"]}>
<FieldItem>
<HStack gap={2}>
<Checkbox value="email" nativeButton render={<button aria-label="Email" />} />
<FieldLabel>Email</FieldLabel>
</HStack>
</FieldItem>
<FieldItem>
<HStack gap={2}>
<Checkbox value="sms" nativeButton render={<button aria-label="SMS" />} />
<FieldLabel>SMS</FieldLabel>
</HStack>
</FieldItem>
</CheckboxGroup>
{state.includes("invalid") && <FieldError>Select a notification method.</FieldError>}
</Field>
))}
</VStack>
)
}API reference
CheckboxGroup accepts Base UI Checkbox Group props, including render, plus xstyle, className, and inline style.
checkboxGroupStyles
| Key | What it is |
|---|---|
root | Vertical layout with space between choices |
Data attributes
| Attribute | On | Effect |
|---|---|---|
data-slot="checkbox-group" | root | Group anatomy |
data-disabled | root | Disabled state |
data-valid, data-invalid, data-touched, data-dirty, data-filled, data-focused | root | Field state |
See Checkbox for individual control attributes.