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

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

@radix-ui/react-checkbox
lucide-react
utils

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.

Copied!
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`.

Packages

checkbox
label

Props

No props documented yet.

Copied!
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.

Packages

checkbox
label

Props

No props documented yet.

Copied!
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.

Packages

checkbox
label

Props

No props documented yet.

Copied!
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]]`.

Packages

checkbox
label

Props

No props documented yet.

Copied!
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.

Packages

react-hook-form
zod
@hookform/resolvers
checkbox
label
button

Props

No props documented yet.

Copied!
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`).

Packages

checkbox
label

Props

No props documented yet.

Copied!
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 & 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

checkbox
label

Props

No props documented yet.

Copied!
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>
  )
}

Frequently asked questions

Yes — it is the canonical shadcn/ui checkbox built on Radix, with the same tokens and aria-invalid error handling, so it drops into any shadcn/ui project. We add one fix on top: the indeterminate state renders a minus icon instead of a checkmark, plus ready-made select-all, group, card, and React Hook Form examples.
Pass checked="indeterminate" instead of true or false. The control then renders a minus icon for the mixed state. The Select all example shows the common pattern: the parent is indeterminate when only some of its children are checked, true when all are, and false when none are.
Track the selected items in state. Make the header checkbox checked when every item is selected, indeterminate when only some are, and false when none are. Toggling it selects or clears the whole list. See the Select all example for the full code.
Register a boolean field, then drive the checkbox with that value and call setValue (or field.onChange) from onCheckedChange. Pair it with a Zod schema that refines the value to be true for required opt-ins, and set aria-invalid from the field error. The React Hook Form example is a complete, copy-pasteable form.
The checkbox does not use a native onChange — use onCheckedChange, which receives true, false, or "indeterminate". Store that in state for a controlled checkbox, or read defaultChecked for an uncontrolled one.
Yes. It is styled with CSS variable tokens like --primary, --ring, and --destructive, so it follows your theme automatically, including dark mode and any palette from a shadcn-compatible theme generator.