Checkbox Group

Shares selection state across a set of checkboxes.

Apples
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/stylex

Set 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.

components/ui/checkbox-group.tsx
"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.

Show these items on the desktop:

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.

Allowed network protocols
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.

Allowed network protocols

Select one or more protocols.

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.

Notifications

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.

disabled
invalid
disabled-invalid
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

KeyWhat it is
rootVertical layout with space between choices

Data attributes

AttributeOnEffect
data-slot="checkbox-group"rootGroup anatomy
data-disabledrootDisabled state
data-valid, data-invalid, data-touched, data-dirty, data-filled, data-focusedrootField state

See Checkbox for individual control attributes.