Shadcn Checkbox for React and Tailwind
Control for multiple selections in a set.
You'll receive a notification when someone posts a comment
Installation
bunx --bun shadcn@latest add https://kit.dev/r/checkbox.jsonpnpm dlx shadcn@latest add https://kit.dev/r/checkbox.jsonnpx shadcn@latest add https://kit.dev/r/checkbox.jsonyarn shadcn@latest add https://kit.dev/r/checkbox.json<Step>This component is typically labeled with Field. Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react tailwind-variants lucide-reactpnpm add @ark-ui/react tailwind-variants lucide-reactnpm install @ark-ui/react tailwind-variants lucide-reactyarn add @ark-ui/react tailwind-variants lucide-reactCopy and paste the following code into your project.
"use client";
import {
Checkbox as ArkCheckbox,
useCheckbox as useArkCheckbox,
useCheckboxContext as useArkCheckboxContext,
useCheckboxGroup as useArkCheckboxGroup,
useCheckboxGroupContext as useArkCheckboxGroupContext,
} from "@ark-ui/react/checkbox";
import { CheckIcon, MinusIcon } from "lucide-react";
import type React from "react";
import { tv } from "tailwind-variants";
import { cn } from "@/lib/utils";
export const useCheckbox = useArkCheckbox;
export const useCheckboxContext = useArkCheckboxContext;
export const CheckboxContext = ArkCheckbox.Context;
export const useCheckboxGroup = useArkCheckboxGroup;
export const useCheckboxGroupContext = useArkCheckboxGroupContext;
const checkboxGroupClassName = "flex flex-col gap-2";
export const CheckboxGroup = (
props: React.ComponentProps<typeof ArkCheckbox.Group>
) => {
const { className, ...rest } = props;
return (
<ArkCheckbox.Group
className={cn(checkboxGroupClassName, className)}
data-slot="checkbox-group"
{...rest}
/>
);
};
export const CheckboxGroupProvider = (
props: React.ComponentProps<typeof ArkCheckbox.GroupProvider>
) => {
const { className, ...rest } = props;
return (
<ArkCheckbox.GroupProvider
className={cn(checkboxGroupClassName, className)}
data-slot="checkbox-group"
{...rest}
/>
);
};
export const checkboxVariants = tv({
base: [
"relative",
"inline-flex shrink-0 items-center justify-center",
"size-4",
"bg-transparent",
"rounded-sm border border-input shadow-xs/5",
"transition-shadow",
"data-focus-visible:border-primary data-focus-visible:ring-[3px] data-focus-visible:ring-ring/32 data-focus-visible:ring-offset-1 data-focus-visible:ring-offset-background",
"dark:data-focus-visible:data-invalid:border-destructive-foreground/64 dark:data-focus-visible:data-invalid:ring-destructive-foreground/48",
"data-disabled:opacity-64",
"[[data-disabled],[data-checked],[data-invalid]]:shadow-none",
"data-invalid:border-destructive data-invalid:ring-[3px] data-invalid:ring-destructive/24",
"dark:data-invalid:border-destructive-foreground dark:data-invalid:text-destructive-foreground dark:data-invalid:ring-destructive-foreground/20",
"dark:not-data-checked:bg-input/32 dark:data-invalid:ring-destructive-foreground/24",
"motion-reduce:transition-none!",
],
});
const CheckboxControl = (props: { tabIndex?: number }) => {
const { tabIndex } = props;
return (
<>
<ArkCheckbox.Control data-slot="checkbox-control">
<CheckboxIndicator>
<CheckIcon aria-hidden="true" />
</CheckboxIndicator>
<CheckboxIndicator indeterminate>
<MinusIcon aria-hidden="true" />
</CheckboxIndicator>
</ArkCheckbox.Control>
<ArkCheckbox.HiddenInput tabIndex={tabIndex} />
</>
);
};
export const Checkbox = (
props: React.ComponentProps<typeof ArkCheckbox.Root>
) => {
const { className, tabIndex, ...rest } = props;
return (
<ArkCheckbox.Root
className={cn(checkboxVariants(), className)}
data-slot="checkbox"
role="checkbox"
{...rest}
>
<CheckboxControl tabIndex={tabIndex} />
</ArkCheckbox.Root>
);
};
export const CheckboxRootProvider = (
props: React.ComponentProps<typeof ArkCheckbox.RootProvider>
) => {
const { className, tabIndex, ...rest } = props;
return (
<ArkCheckbox.RootProvider
className={cn(checkboxVariants(), className)}
data-slot="checkbox"
role="checkbox"
{...rest}
>
<CheckboxControl tabIndex={tabIndex} />
</ArkCheckbox.RootProvider>
);
};
export const CheckboxIndicator = (
props: React.ComponentProps<typeof ArkCheckbox.Indicator>
) => {
const { className, ...rest } = props;
return (
<ArkCheckbox.Indicator
className={cn(
"absolute -inset-px",
"flex items-center justify-center",
"rounded-sm",
"text-primary-foreground",
"data-[state=checked]:bg-primary",
"data-[state=unchecked]:hidden",
"data-[state=indeterminate]:text-foreground",
className
)}
data-slot="checkbox-indicator"
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
Checkbox
├── Control (baked in)
│ ├── Indicator (checked)
│ └── Indicator (indeterminate)
└── HiddenInput (baked in)
CheckboxGroup
└── CheckboxCheckbox is Ark Checkbox.Root (a label). Control, indicators, and the hidden native input are baked in. Put the visible name on Field / FieldLabel, not inside the box.
useCheckbox is the machine hook for CheckboxRootProvider. useCheckboxContext / CheckboxContext is in-tree. useCheckboxGroup is for CheckboxGroupProvider.
Usage
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldLabel } from "@/components/ui/field";<Field orientation="horizontal">
<Checkbox />
<FieldLabel>Accept terms and conditions</FieldLabel>
</Field>Do not wrap Checkbox in another label. The root is already a label associated with the hidden input. FieldLabel uses Field's htmlFor (not a nested label around the box).
Controlled
Use checked and onCheckedChange. checked is boolean | "indeterminate".
Default checked
Uncontrolled initial state with defaultChecked.
Root Provider
Use useCheckbox with CheckboxRootProvider when you need the API outside the tree. Pass machine options (defaultChecked, disabled, name, …) to useCheckbox(), not to the provider.
Context
CheckboxContext / useCheckboxContext only work under Checkbox or CheckboxRootProvider. shadcn.io bakes the control into a size-4 root, so the usual way to read state outside the box is useCheckbox + CheckboxRootProvider.
States
Invalid
Set invalid on Checkbox or on Field (Field forwards invalid into the checkbox).
Disabled
Read-only
readOnly keeps the value visible but not changeable.
Indeterminate
Set checked="indeterminate" (not a separate indeterminate prop on the root). The baked indicator uses MinusIcon for that state.
Examples
With description
Field helper and error
Card
Wrap Field in FieldLabel for a full-width selectable card.
Form
Pass name and value (default "on"). The hidden input submits with the form. Unchecked boxes are omitted from FormData.
Library-specific forms (React Hook Form, TanStack Form, Formisch) live on the form pages.
Checkbox Group
CheckboxGroup owns the selected values. Each Checkbox needs a value. The group sets name, disabled, invalid, and readOnly on items.
<CheckboxGroup defaultValue={["hard-disks"]} name="desktop">
<Field orientation="horizontal">
<Checkbox value="hard-disks" />
<FieldLabel>Hard disks</FieldLabel>
</Field>
</CheckboxGroup>Controlled group
Use value and onValueChange. The handler receives string[], not a details object.
Group provider
useCheckboxGroup + CheckboxGroupProvider when you need the group API outside the tree.
Invalid group
invalid on CheckboxGroup applies to every item.
Max selected
maxSelectedValues disables remaining items once the cap is reached.
Select all
A parent checkbox outside the group, with checked="indeterminate" when the selection is partial.
Group in a form
Set name on the group. Read selected values with FormData.getAll(name).
Guides
asChild
The root is a label. If you set asChild, the child must also be a label or the HTML and accessibility structure is invalid.
Pages vs Field
Prefer Field + FieldLabel for the visible name. Do not use Ark Checkbox.Label inside the shadcn.io box — the root is already the visual control.
API Reference
shadcn.io wraps Ark UI Checkbox. Control, checked/indeterminate indicators, and HiddenInput are baked into Checkbox / CheckboxRootProvider.
asChild merges props onto a single child element.
Checkbox
Root. Renders a label. shadcn.io also sets role="checkbox" so Field horizontal alignment can target the box.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the child element instead of a label. The child must be a label. |
checked | boolean | "indeterminate" | - | Controlled checked state. |
className | string | - | Class names on the root (the visual box). |
defaultChecked | boolean | "indeterminate" | - | Uncontrolled initial state. |
dir | "ltr" | "rtl" | - | Text direction. Usually inherited from LocaleProvider. |
disabled | boolean | - | Disable the checkbox. Also inherited from Field / CheckboxGroup. |
form | string | - | form id for the hidden input. |
id | string | - | Unique id for the checkbox machine. |
ids | Partial<{ root: string; hiddenInput: string; control: string; label: string }> | - | Element ids for composition. Field sets label and hiddenInput when present. |
invalid | boolean | - | Invalid state. Also inherited from Field / CheckboxGroup. |
name | string | - | Hidden input name. Inherited from CheckboxGroup when grouped. |
onCheckedChange | (details: CheckedChangeDetails) => void | - | Checked state changed. { checked: boolean | "indeterminate" }. |
readOnly | boolean | - | Visible but not editable. |
required | boolean | - | Native required. Also inherited from Field. |
tabIndex | number | - | Applied to the hidden input, not the label. |
value | string | "on" | Hidden input value. Required for items in a group. |
| Attribute | Description |
|---|---|
data-slot | checkbox |
data-scope | checkbox |
data-part | root |
data-state | "checked", "unchecked", or "indeterminate" |
data-focus | Present when focused |
data-focus-visible | Present when focus is visible |
data-hover | Present when hovered |
data-active | Present while pressed |
data-disabled | Present when disabled |
data-invalid | Present when invalid |
data-readonly | Present when read-only |
data-required | Present when required |
The native input is visually hidden. The control is aria-hidden; check and minus icons are aria-hidden.
Control / Indicator / HiddenInput
Baked into Checkbox and CheckboxRootProvider. Not used as separate imports in typical usage. CheckboxIndicator remains exported if you replace the chrome.
| Attribute | Description |
|---|---|
data-slot | checkbox-control / checkbox-indicator |
data-scope | checkbox |
data-part | control / indicator |
data-state | Same as the root |
CheckboxIndicator accepts indeterminate to show only in the indeterminate state.
CheckboxRootProvider
Takes the API from useCheckbox. Same baked control as Checkbox. Renders a label.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseCheckboxReturn | required | Return value of useCheckbox(). |
asChild | boolean | false | Render the child element instead of a label. |
className | string | - | Class names on the root. |
tabIndex | number | - | Applied to the hidden input. |
Pass defaultChecked, disabled, name, and other machine options to useCheckbox(), not to CheckboxRootProvider.
CheckboxGroup
Manages a set of checkboxes. Renders a div with role="group".
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the group. |
defaultValue | string[] | [] | Uncontrolled selected values. |
disabled | boolean | - | Disable every item. Also inherited from Fieldset. |
invalid | boolean | - | Invalid state on every item. Also inherited from Fieldset. |
maxSelectedValues | number | - | Cap on selected values. Further items become disabled. |
name | string | - | Shared name for hidden inputs. |
onValueChange | (value: string[]) => void | - | Selected values changed. Receives string[], not { value }. |
readOnly | boolean | - | Read-only on every item. |
value | string[] | - | Controlled selected values. |
| Attribute | Description |
|---|---|
data-slot | checkbox-group |
data-scope | checkbox |
data-part | group |
CheckboxGroupProvider
Takes the API from useCheckboxGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseCheckboxGroupReturn | required | Return value of useCheckboxGroup(). |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the group. |
Pass defaultValue, name, maxSelectedValues, and other options to useCheckboxGroup(), not to the provider.
useCheckbox
Creates the checkbox API for CheckboxRootProvider. Accepts the same machine options as Checkbox except layout-only props.
const checkbox = useCheckbox({ defaultChecked: true });
checkbox.setChecked(false);
checkbox.toggleChecked();Inherits disabled, invalid, required, and ids from Field when used under Field. Inherits item props from CheckboxGroup when grouped.
CheckboxContext / useCheckboxContext
Render-prop or hook access inside Checkbox or CheckboxRootProvider.
| Property | Type | Description |
|---|---|---|
checked | boolean | Whether the box is fully checked (false when indeterminate). |
checkedState | boolean | "indeterminate" | The full checked state. |
indeterminate | boolean | Whether the state is indeterminate. |
disabled | boolean | Whether the checkbox is disabled. |
focused | boolean | Whether the hidden input is focused. |
setChecked | (checked: boolean | "indeterminate") => void | Set the checked state. |
toggleChecked | () => void | Toggle checked. |
CheckboxContext children: (context) => ReactNode.
useCheckboxGroup / useCheckboxGroupContext
useCheckboxGroup creates the group store for CheckboxGroupProvider. useCheckboxGroupContext reads it inside a group.
| Property | Type | Description |
|---|---|---|
value | string[] | Selected values. |
name | string | undefined | Shared input name. |
disabled / readOnly / invalid | boolean | Group flags. |
setValue | (value: string[]) => void | Replace the selection. |
addValue / toggleValue | (value: string) => void | Add or toggle one value. |
isChecked | (value: string | undefined) => boolean | Whether a value is selected. |
getItemProps | (props: { value?: string }) => … | Props merged into each checkbox. |
Accessibility
Complies with the Checkbox WAI-ARIA design pattern. The hidden native input type="checkbox" is the accessible control. The root label is associated with it. Give every checkbox a visible name (FieldLabel). Groups should have a legend (FieldLegend / FieldSet).
Prefer Field so disabled, invalid, and required stay in sync. Decorative check and minus icons are aria-hidden.
Keyboard support
| Key | Description |
|---|---|
Tab | Move focus to the checkbox (the hidden input). |
Shift + Tab | Move focus to the previous control. |
Space | Toggle checked. From indeterminate, typically becomes checked. |