Lets users turn a single option on or off, or pick any number of options from a group. Supports descriptions, an indeterminate state, card layouts, validation and native form submission.
import { Checkbox } from "@/components/ui/checkbox";export default function CheckboxDemo() { return <Checkbox defaultSelected>Email me about product updates</Checkbox>;}
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";
tsx
<Checkbox>Remember me</Checkbox><CheckboxGroup label="Notify me about" defaultValue={["mentions"]}> <Checkbox value="mentions">Mentions</Checkbox> <Checkbox value="comments">Comments</Checkbox></CheckboxGroup>
A standalone Checkbox owns a boolean (isSelected). Inside a CheckboxGroup, each checkbox needs a value and the group owns a string[] of the selected values.
Checkbox is built on React Aria's CheckboxField and CheckboxButton. You only render Checkbox; it composes the parts for you:
tsx
<CheckboxGroup> {/* label, description, error message */} <Checkbox value="…"> {/* CheckboxField: <div>, owns state and validation */} {/* CheckboxButton: <label> wrapping a hidden <input type="checkbox"> */} {/* box + children + description (Text slot="description") */} {/* FieldError: standalone checkboxes only */} </Checkbox></CheckboxGroup>
Part
Renders
Notes
Checkbox root (CheckboxField)
<div data-slot="checkbox-field">
Holds the checked state, name / value, validation and the ARIA wiring between the input, description and error. Carries data-selected, data-indeterminate, data-disabled, data-readonly, data-invalid, data-required.
The clickable area: box, label and description. Receives className, style, render, children and hover handlers, and carries every interaction attribute.
Box
<span data-slot="checkbox-indicator">
Shows a check icon, or a minus icon when isIndeterminate. Sized by size.
Description
<span slot="description">
From the description prop. Rendered inside the label so the whole area stays clickable, and linked to the input with aria-describedby.
FieldError
<div data-slot="field-error">
Standalone checkboxes only: shows errorMessage or the browser message when validation fails. Inside a group, the group shows one error instead.
CheckboxGroup
<div role="group">
Wraps the label, description, the checkboxes and the error message.
Group label / description
<span>
Rendered from the group's label and description props.
Checked, unchecked and indeterminate are the three visual values. isInvalid turns the box red, isDisabled dims it and removes it from the tab order, and isReadOnly keeps it focusable but ignores changes.
description adds secondary text under the label and aligns the box to the first line. Use it to explain the consequence of an option instead of a long label.
import { Checkbox } from "@/components/ui/checkbox";export default function CheckboxDescription() { return ( <Checkbox className="max-w-xs" defaultSelected description="Get a weekly digest of activity in your workspace." > Email notifications </Checkbox> );}
CheckboxGroup renders a shared label and description, gives the checkboxes role="group", and collects the selected values into an array.
Notify me aboutWe'll only email you about the things you pick.
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";export default function CheckboxGroupDemo() { return ( <CheckboxGroup label="Notify me about" description="We'll only email you about the things you pick." defaultValue={["mentions", "comments"]} > <Checkbox value="mentions">Mentions</Checkbox> <Checkbox value="comments">Replies to my comments</Checkbox> <Checkbox value="assigned">Issues assigned to me</Checkbox> <Checkbox value="updates">Product updates</Checkbox> </CheckboxGroup> );}
orientation="horizontal" lays the checkboxes out in a wrapping row. Keep labels short; switch back to vertical as soon as options wrap to a second line.
isDisabled on a single checkbox locks just that option; use the description to say why. isReadOnly on the group shows the current value without letting users change it, and keeps every option focusable.
Email alertsSecurity alerts are required by your organization.
Included in your plan
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";export default function CheckboxGroupDisabled() { return ( <div className="flex flex-col gap-8"> <CheckboxGroup label="Email alerts" description="Security alerts are required by your organization." defaultValue={["security", "billing"]} > <Checkbox value="security" isDisabled> Security alerts </Checkbox> <Checkbox value="billing">Billing and invoices</Checkbox> <Checkbox value="digest">Weekly digest</Checkbox> </CheckboxGroup> <CheckboxGroup label="Included in your plan" isReadOnly defaultValue={["sso", "audit"]} > <Checkbox value="sso">Single sign-on</Checkbox> <Checkbox value="audit">Audit log</Checkbox> <Checkbox value="scim">SCIM provisioning</Checkbox> </CheckboxGroup> </div> );}
A parent checkbox for a set of children. Set isIndeterminate when only some children are selected. It's purely visual (announced as "mixed"), so you must control it; pressing the checkbox calls onChange with the next value.
className is merged with the base styles, so a border, padding and data-selected: styles turn a checkbox with a description into a selectable card. The whole card is the label, so it's all clickable.
isRequired on a group requires at least one selection; on a standalone checkbox it requires it to be checked. With the default native validation, submitting the form shows the errorMessage (or the browser message) under the group or the standalone checkbox and focuses the first invalid field. A required group also adds a * to its label.
import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";export default function CheckboxValidation() { return ( <Form className="flex w-full max-w-xs flex-col gap-5" onSubmit={(e) => e.preventDefault()} > <CheckboxGroup label="How did you hear about us?" isRequired errorMessage="Pick at least one option." > <Checkbox value="search">Search engine</Checkbox> <Checkbox value="friend">A friend or colleague</Checkbox> <Checkbox value="social">Social media</Checkbox> </CheckboxGroup> <Checkbox isRequired>I agree to the Terms of Service</Checkbox> <Button type="submit" className="self-start"> Continue </Button> </Form> );}
validate receives the selected values and returns an error string, or null when valid. With validationBehavior="aria" the error shows as soon as the value changes instead of on submit.
Topics to followChoose up to three.
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";const topics = [ "Design systems", "Accessibility", "Performance", "Testing", "Animation",];export default function CheckboxCustomValidation() { return ( <CheckboxGroup label="Topics to follow" description="Choose up to three." defaultValue={["Accessibility"]} validationBehavior="aria" validate={(value) => value.length > 3 ? "You can follow up to three topics." : null } > {topics.map((topic) => ( <Checkbox key={topic} value={topic}> {topic} </Checkbox> ))} </CheckboxGroup> );}
Give the group a name and every checked value is submitted under it; read them with FormData.getAll(). A standalone checkbox submits its value ("on" by default) only when checked.
import { useState } from "react";import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox";export default function CheckboxForm() { const [result, setResult] = useState<string | null>(null); return ( <Form className="flex w-full max-w-xs flex-col gap-5" onSubmit={(e) => { e.preventDefault(); const data = new FormData(e.currentTarget); setResult( JSON.stringify({ toppings: data.getAll("toppings"), extraNapkins: data.get("napkins"), }), ); }} > <CheckboxGroup label="Toppings" name="toppings" defaultValue={["cheese"]}> <Checkbox value="cheese">Extra cheese</Checkbox> <Checkbox value="mushrooms">Mushrooms</Checkbox> <Checkbox value="olives">Olives</Checkbox> </CheckboxGroup> <Checkbox name="napkins" value="yes"> Extra napkins </Checkbox> <Button type="submit" className="self-start"> Place order </Button> {result && ( <code className="rounded-md bg-muted px-2 py-1 text-xs">{result}</code> )} </Form> );}
A select-all checkbox in a list header, indeterminate while only some rows are selected, with bulk actions that appear once anything is selected. Row checkboxes have no visible label, so each gets an aria-label.
Each checkbox is a native <input type="checkbox">, visually hidden inside a <label>, so clicking anywhere on the label toggles it and it works with every assistive technology and with native forms.
isIndeterminate sets the input's indeterminate property, which screen readers announce as "mixed".
Checkboxes without visible text (table rows, matrices) need an aria-label or aria-labelledby.
CheckboxGroup renders role="group" labelled by its label; its description and error message are linked with aria-describedby.
description on a Checkbox is linked to the input with aria-describedby, so it's announced as the description rather than as part of the name. It sits inside the <label> (so it's clickable) but is hidden from the name computation.
A standalone checkbox's error message is linked to the input with aria-describedby as well.
The focus ring only appears for keyboard focus (data-focus-visible).
The attributes you'll style against are on the <label> (CheckboxButton), which is also where className goes. Target the box or label text from it with group-data-*/checkbox:. The outer CheckboxField<div> repeats the state attributes (not the interaction ones) and is a group/checkbox-field, for styling siblings such as the error message.
Attribute
<label> (CheckboxButton)
<div> (CheckboxField)
Present when
data-selected
Yes
Yes
Checked
data-indeterminate
Yes
Yes
isIndeterminate is true
data-hovered
Yes
No
Hovered with a mouse or pen
data-pressed
Yes
No
Being pressed
data-focused / data-focus-visible
Yes
No
Focused / focused with the keyboard
data-disabled
Yes
Yes
Disabled, directly or through the group
data-readonly
Yes
Yes
Read-only, directly or through the group
data-invalid
Yes
Yes
Invalid, directly or through the group
data-required
Yes
Yes
Required
On CheckboxGroup (children can use group-data-*/field:): data-disabled, data-readonly, data-required, data-invalid.
The box uses the --brand and --brand-foreground theme variables (red --destructive when invalid). Override them on a single checkbox or a whole group for a one-off color:
tsx
<Checkbox className="[--brand:var(--color-emerald-600)] [--brand-foreground:white]"> Mark as paid</Checkbox>
className and children accept a function of the CheckboxButton state (isSelected, isIndeterminate, isHovered, isPressed, isFocusVisible, isDisabled, isReadOnly, isInvalid, isRequired):
Also accepts every prop of React Aria's CheckboxField. className, style, render, children and the hover handlers go to the CheckboxButton instead; everything else (state, name, value, validation, focus and press handlers, ARIA and DOM attributes) goes to the CheckboxField.