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

Source: https://designrevision.com/components/checkbox

The **Shadcn Checkbox** is the control for binary and multi-select choices — the canonical <a href="https://ui.shadcn.com/docs/components/checkbox" rel="nofollow">shadcn/ui</a> checkbox built on <a href="https://www.radix-ui.com/primitives/docs/components/checkbox" rel="nofollow">Radix</a>, themed entirely with CSS variables. It pairs with a [Label](/components/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](/components/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 <a href="https://react-hook-form.com" rel="nofollow">React Hook Form</a>: 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.

## Installation

### checkbox

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox
```

**Dependencies:** @radix-ui/react-checkbox, lucide-react, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checked | boolean \| 'indeterminate' | — | Controlled checked state. Pass 'indeterminate' for the mixed (partial) state — it renders a minus instead of a check. |
| defaultChecked | boolean | false | Initial checked state when uncontrolled. |
| onCheckedChange | (checked: boolean \| 'indeterminate') => void | — | Fires when the user toggles the checkbox. |
| disabled | boolean | false | Prevents interaction and dims the control. |

**Usage & accessibility:** 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.

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

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-label-01
```

**Dependencies:** checkbox, label

```tsx
// components/ui/checkbox-label-01.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-group-01
```

**Dependencies:** checkbox, label

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

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-select-all-01
```

**Dependencies:** checkbox, label

```tsx
// components/ui/checkbox-select-all-01.tsx
"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]]`.

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-card-01
```

**Dependencies:** checkbox, label

```tsx
// components/ui/checkbox-card-01.tsx
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.

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-form-01
```

**Dependencies:** react-hook-form, zod, @hookform/resolvers, checkbox, label, button

```tsx
// components/ui/checkbox-form-01.tsx
"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`).

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-states-01
```

**Dependencies:** checkbox, label

```tsx
// components/ui/checkbox-states-01.tsx
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 &amp; 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/checkbox-sizes-01
```

**Dependencies:** checkbox, label

```tsx
// components/ui/checkbox-sizes-01.tsx
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>
  )
}
```
