Shadcn Checkbox
Free shadcn/ui checkbox component for React — indeterminate select-all, checkbox groups, React Hook Form validation and card examples. Copy or install in one command.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
checked
= —
boolean | 'indeterminate'
Controlled checked state. Pass 'indeterminate' for the mixed (partial) state — it renders a minus instead of a check.
defaultChecked
= false
boolean
Initial checked state when uncontrolled.
onCheckedChange
= —
(checked: boolean | 'indeterminate') => void
Fires when the user toggles the checkbox.
disabled
= false
boolean
Prevents interaction and dims the control.
Extends the Radix Checkbox Root (@radix-ui/react-checkbox). Set aria-invalid for the destructive error styling. The indicator shows a check when checked and a minus when indeterminate.
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon, MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer group border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current"
>
<CheckIcon className="size-3.5 group-data-[state=indeterminate]:hidden" />
<MinusIcon className="hidden size-3.5 group-data-[state=indeterminate]:block" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
Examples
With label
The canonical opt-in field — a `Label` and description bound to a controlled checkbox via `htmlFor`, toggled with `onCheckedChange`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
export default function CheckboxLabel01() {
const [checked, setChecked] = React.useState(true)
return (
<div className="flex items-start gap-3">
<Checkbox
id="terms"
checked={checked}
onCheckedChange={(value) => setChecked(value === true)}
/>
<div className="grid gap-1.5">
<Label htmlFor="terms">Accept terms and conditions</Label>
<p className="text-sm text-muted-foreground">
You agree to our Terms of Service and Privacy Policy.
</p>
</div>
</div>
)
}
Group
A set of related checkboxes wrapped in a `fieldset` with a `legend` — the accessible way to group multiple options with descriptions.
Packages
Props
No props documented yet.
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
const items = [
{
id: "comments",
label: "Comments",
description: "Get notified when someone comments on your post.",
},
{
id: "mentions",
label: "Mentions",
description: "Get notified when someone @mentions you.",
},
{
id: "follows",
label: "Follows",
description: "Get notified when someone follows you.",
},
]
export default function CheckboxGroup01() {
return (
<fieldset className="grid w-full max-w-sm gap-4">
<legend className="mb-1 text-sm font-medium text-foreground">
Email notifications
</legend>
{items.map((item) => (
<div key={item.id} className="flex items-start gap-3">
<Checkbox id={item.id} defaultChecked={item.id !== "follows"} />
<div className="grid gap-1.5">
<Label htmlFor={item.id}>{item.label}</Label>
<p className="text-sm text-muted-foreground">{item.description}</p>
</div>
</div>
))}
</fieldset>
)
}
Select all (indeterminate)
A parent checkbox that renders the **indeterminate** state — a minus, not a check — when only some children are selected. Checking it toggles the whole group.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
const tasks = [
{ id: "task-1", label: "Review pull request" },
{ id: "task-2", label: "Update documentation" },
{ id: "task-3", label: "Deploy to staging" },
]
export default function CheckboxSelectAll01() {
const [checked, setChecked] = React.useState<string[]>(["task-1"])
const allChecked = checked.length === tasks.length
const someChecked = checked.length > 0 && !allChecked
return (
<div className="grid w-full max-w-sm gap-3">
<div className="flex items-center gap-3 border-b border-border pb-3">
<Checkbox
id="select-all"
checked={allChecked ? true : someChecked ? "indeterminate" : false}
onCheckedChange={(value) =>
setChecked(value === true ? tasks.map((t) => t.id) : [])
}
/>
<Label htmlFor="select-all" className="font-medium">
Select all
</Label>
</div>
{tasks.map((task) => (
<div key={task.id} className="flex items-center gap-3">
<Checkbox
id={task.id}
checked={checked.includes(task.id)}
onCheckedChange={(value) =>
setChecked((prev) =>
value === true
? [...prev, task.id]
: prev.filter((t) => t !== task.id)
)
}
/>
<Label htmlFor={task.id} className="font-normal text-muted-foreground">
{task.label}
</Label>
</div>
))}
</div>
)
}
Cards
Whole-card selectable options with a title and description that highlight when checked, using `has-[[data-state=checked]]`.
Packages
Props
No props documented yet.
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
const plans = [
{
id: "plan-starter",
title: "Starter",
description: "For individuals getting started.",
},
{
id: "plan-pro",
title: "Pro",
description: "For small teams that need more room to grow.",
},
]
export default function CheckboxCard01() {
return (
<div className="grid w-full max-w-sm gap-3">
{plans.map((plan) => (
<Label
key={plan.id}
htmlFor={plan.id}
className="flex items-start gap-3 rounded-lg border border-border p-4 has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/5"
>
<Checkbox
id={plan.id}
defaultChecked={plan.id === "plan-pro"}
className="mt-0.5"
/>
<div className="grid gap-1.5 font-normal">
<span className="text-sm font-medium leading-none">{plan.title}</span>
<span className="text-sm text-muted-foreground">
{plan.description}
</span>
</div>
</Label>
))}
</div>
)
}
React Hook Form
A required “accept terms” checkbox validated with `react-hook-form` and `zod`, showing an `aria-invalid` error message until it is checked.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
const schema = z.object({
terms: z.boolean().refine((value) => value === true, {
message: "You must accept the terms to continue.",
}),
})
type FormValues = z.infer<typeof schema>
export default function CheckboxForm01() {
const [submitted, setSubmitted] = React.useState(false)
const {
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { terms: false },
})
const terms = watch("terms")
return (
<form
onSubmit={handleSubmit(() => setSubmitted(true))}
className="grid w-full max-w-sm gap-4"
>
<div className="flex items-start gap-3">
<Checkbox
id="accept"
checked={terms}
onCheckedChange={(value) =>
setValue("terms", value === true, { shouldValidate: true })
}
aria-invalid={!!errors.terms}
aria-describedby={errors.terms ? "accept-error" : undefined}
/>
<div className="grid gap-1.5">
<Label htmlFor="accept">Accept terms and conditions</Label>
<p className="text-sm text-muted-foreground">
You agree to our Terms of Service and Privacy Policy.
</p>
{errors.terms ? (
<p id="accept-error" className="text-sm text-destructive">
{errors.terms.message}
</p>
) : null}
</div>
</div>
<Button type="submit" className="w-fit">
{submitted ? "Submitted!" : "Submit"}
</Button>
</form>
)
}
States
Checked, disabled, disabled-and-checked, and invalid (`aria-invalid` switches on the destructive ring, paired with `aria-describedby`).
Packages
Props
No props documented yet.
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
export default function CheckboxStates01() {
return (
<div className="grid w-full max-w-sm gap-5">
<div className="flex items-center gap-3">
<Checkbox id="state-default" defaultChecked />
<Label htmlFor="state-default">Checked by default</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox id="state-disabled" disabled />
<Label htmlFor="state-disabled">Disabled</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox id="state-disabled-checked" defaultChecked disabled />
<Label htmlFor="state-disabled-checked">Disabled & checked</Label>
</div>
<div className="grid gap-1.5">
<div className="flex items-center gap-3">
<Checkbox
id="state-invalid"
aria-invalid
aria-describedby="state-invalid-error"
/>
<Label htmlFor="state-invalid">Required — please accept</Label>
</div>
<p id="state-invalid-error" className="text-sm text-destructive">
This field is required.
</p>
</div>
</div>
)
}
Sizes
Small, default, and large checkboxes — the size scales from the `size-*` class, with the indicator icon following.
Packages
Props
No props documented yet.
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
export default function CheckboxSizes01() {
return (
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Checkbox
id="size-sm"
defaultChecked
className="size-3.5 rounded-[3px] [&_svg]:size-3"
/>
<Label htmlFor="size-sm" className="text-xs">
Small
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="size-default" defaultChecked />
<Label htmlFor="size-default">Default</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="size-lg" defaultChecked className="size-5 [&_svg]:size-4" />
<Label htmlFor="size-lg" className="text-base">
Large
</Label>
</div>
</div>
)
}
The Shadcn Checkbox is the control for binary and multi-select choices — the canonical shadcn/ui checkbox built on Radix, themed entirely with CSS variables. It pairs with a Label for a complete field, and it ships one fix the original leaves out: the indeterminate state renders a minus, not a checkmark. The examples below cover the patterns you actually build — groups, select-all, cards, and validated forms.
When to use
Reach for a checkbox when each option is independent — accept terms, toggle a setting, pick any number of items from a list. For one choice out of several, use a radio group; for an instant on/off with no submit, a switch often reads better. A single checkbox should always have a Label; a set of related checkboxes belongs in a fieldset with a legend.
Indeterminate and select-all
Pass checked="indeterminate" for the mixed state and the control renders a minus icon. The classic use is a select-all header over a list: it's true when every row is selected, "indeterminate" when only some are, and false when none are — and toggling it selects or clears the whole group. This is the pattern behind table headers and bulk-action lists; the Select-all example is the complete, copy-pasteable version.
Groups and accessibility
Group related checkboxes in a fieldset with a legend so screen readers announce the set as a unit, and bind every checkbox to its own Label with htmlFor/id. The checkbox uses the standard aria-invalid attribute for errors — set it and the destructive ring and border switch on automatically — and you should pair an invalid field with a visible message linked by aria-describedby.
React Hook Form
The checkbox is fully controllable, so it slots into React Hook Form: drive the control's checked from the field value and call setValue (or field.onChange) from onCheckedChange. Combined with a zod schema that refines a required opt-in to true, you get inline validation with an aria-invalid error message — see the React Hook Form example for the full, working form.
Theming
The checkbox reads your design tokens (--primary, --ring, --destructive), so it follows your theme — including dark mode — with zero overrides. Any palette produced by a shadcn-compatible theme generator applies automatically.