# Checkbox

> A checkbox whose check draws in, with indeterminate parents, groups and labels that share its hover.

Docs: https://hextaui.com/docs/checkbox
Markdown: https://hextaui.com/docs/checkbox.md

```tsx title="components/examples/checkbox/demo.tsx"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxDemo() {
  return (
    <div className="flex flex-col gap-3">
      <label className="flex items-center gap-3 text-sm">
        <Checkbox />
        Email me about product updates
      </label>
      <label className="flex items-center gap-3 text-sm">
        <Checkbox defaultChecked />
        Remember this device
      </label>
      <label className="flex max-w-sm items-start gap-3 text-sm">
        <span className="flex h-5 items-center">
          <Checkbox />
        </span>
        <span className="flex flex-col gap-0.5">
          <span className="leading-5 font-medium">Mentions and replies</span>
          <span className="text-muted-foreground">
            We’ll only notify you about activity on your own posts.
          </span>
        </span>
      </label>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/checkbox.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/checkbox.tsx"
"use client"

import * as React from "react"
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group"
import { IconCheck, IconMinus } from "@tabler/icons-react"
import { cn } from "cn"

function mergeClassName<State>(
  base: string,
  className: string | ((state: State) => string | undefined) | undefined
) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const checkboxClassName =
  "peer relative inline-flex size-4 shrink-0 items-center justify-center rounded-[calc(var(--radius-sm)*0.5)] inset-ring-(length:--hairline) forced-colors:border inset-ring-muted-foreground/80 bg-background text-primary-foreground transition-[background-color,border-color,box-shadow,scale] duration-150 ease-out-quint outline-none focus-visible:outline-hidden select-none after:absolute after:-inset-2.5 pointer-coarse:after:-inset-3.5 hover:inset-ring-foreground/70 [@media(hover:hover)]:in-[label:hover]:inset-ring-foreground/70 focus-visible:inset-ring-ring focus-visible:ring-3 focus-visible:ring-focus-ring motion-safe:active:scale-95 motion-safe:in-[label:active]:scale-95 data-checked:inset-ring-primary data-checked:bg-primary data-indeterminate:inset-ring-primary data-indeterminate:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:inset-ring-muted-foreground/80 data-disabled:[@media(hover:hover)]:in-[label:hover]:inset-ring-muted-foreground/80 motion-safe:data-disabled:active:scale-100 motion-safe:data-disabled:in-[label:active]:scale-100 data-readonly:cursor-default motion-safe:data-readonly:active:scale-100 motion-safe:data-readonly:in-[label:active]:scale-100 aria-invalid:inset-ring-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-invalid:inset-ring-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:inset-ring-destructive/50 dark:aria-invalid:ring-destructive/40 dark:data-invalid:inset-ring-destructive/50 dark:data-invalid:ring-destructive/40 data-checked:data-invalid:inset-ring-primary data-checked:aria-invalid:inset-ring-primary dark:data-checked:bg-primary dark:data-indeterminate:bg-primary"

function CheckboxMark({ indeterminate }: { indeterminate: boolean }) {
  const markRef = React.useRef<SVGSVGElement>(null)

  React.useLayoutEffect(() => {
    const mark = markRef.current
    const root = mark?.closest("[data-slot=checkbox]")

    if (mark && root?.hasAttribute("data-mounted")) {
      mark.setAttribute("data-draw", "")
    }
  }, [])

  const Icon = indeterminate ? IconMinus : IconCheck

  return (
    <Icon
      ref={markRef}
      aria-hidden
      stroke={2.75}
      className={cn(
        "pointer-events-none size-3.5 motion-safe:data-draw:animate-checkbox-draw",
        indeterminate
          ? "motion-safe:data-draw:[stroke-dasharray:15] motion-safe:data-draw:[stroke-dashoffset:15]"
          : "motion-safe:data-draw:[stroke-dasharray:22] motion-safe:data-draw:[stroke-dashoffset:22]"
      )}
    />
  )
}

function Checkbox({
  className,
  children,
  ref,
  ...props
}: CheckboxPrimitive.Root.Props) {
  const rootRef = React.useRef<HTMLElement | null>(null)

  const setRoot = React.useCallback(
    (node: HTMLElement | null) => {
      rootRef.current = node
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    },
    [ref]
  )

  React.useEffect(() => {
    const frame = requestAnimationFrame(() => {
      rootRef.current?.setAttribute("data-mounted", "")
    })
    return () => cancelAnimationFrame(frame)
  }, [])

  return (
    <CheckboxPrimitive.Root
      ref={setRoot}
      className={mergeClassName(checkboxClassName, className)}
      {...props}
      data-slot="checkbox"
    >
      <CheckboxPrimitive.Indicator
        data-slot="checkbox-indicator"
        className="grid place-content-center text-current transition-[opacity,scale] duration-100 ease-out-quint data-ending-style:opacity-0 motion-safe:data-ending-style:scale-75"
        render={(indicatorProps, state) => (
          <span {...indicatorProps}>
            <CheckboxMark
              key={state.indeterminate ? "indeterminate" : "checked"}
              indeterminate={state.indeterminate}
            />
          </span>
        )}
      />
      {children}
    </CheckboxPrimitive.Root>
  )
}

function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
  return (
    <CheckboxGroupPrimitive
      className={mergeClassName("flex flex-col gap-3", className)}
      {...props}
      data-slot="checkbox-group"
    />
  )
}

export { Checkbox, CheckboxGroup }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"
```

```tsx
<label className="flex items-center gap-3">
  <Checkbox defaultChecked />
  Remember this device
</label>
```

## Composition

```text
CheckboxGroup
└── Checkbox
```

## Examples

### States

`disabled`, `readOnly`, `indeterminate` and `aria-invalid`. A read-only box keeps its value and stays focusable, but ignores clicks and keys.

```tsx title="components/examples/checkbox/states.tsx"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxStates() {
  return (
    <div className="flex flex-col gap-3 text-sm">
      <label className="flex items-center gap-3">
        <Checkbox disabled />
        Disabled
      </label>
      <label className="flex items-center gap-3">
        <Checkbox disabled defaultChecked />
        Disabled and checked
      </label>
      <label className="flex items-center gap-3">
        <Checkbox readOnly defaultChecked />
        Read-only
      </label>
      <label className="flex items-center gap-3">
        <Checkbox indeterminate />
        Indeterminate
      </label>
      <label className="flex items-center gap-3">
        <Checkbox aria-invalid />
        Invalid
      </label>
      <label className="flex items-center gap-3">
        <Checkbox aria-invalid defaultChecked />
        Invalid and checked
      </label>
    </div>
  )
}
```

### Select all

Inside a `<CheckboxGroup />`, a checkbox with `parent` ticks every value in `allValues`. It turns indeterminate when only some are ticked.

```tsx title="components/examples/checkbox/select-all.tsx"
"use client"

import * as React from "react"

import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"

const fruits = ["Apple", "Banana", "Cherry", "Mango", "Peach"]

export function CheckboxSelectAll() {
  const [value, setValue] = React.useState<string[]>(["Banana"])

  return (
    <div className="text-sm">
      <CheckboxGroup
        aria-label="Fruits"
        value={value}
        onValueChange={setValue}
        allValues={fruits}
      >
        <label className="flex items-center gap-3">
          <Checkbox parent />
          <span>
            Select all{" "}
            <span className="text-muted-foreground">
              ({value.length}/{fruits.length})
            </span>
          </span>
        </label>
        <div className="flex flex-col gap-3 ps-7">
          {fruits.map((fruit) => (
            <label key={fruit} className="flex items-center gap-3">
              <Checkbox value={fruit} />
              {fruit}
            </label>
          ))}
        </div>
      </CheckboxGroup>
    </div>
  )
}
```

### Nested groups

Groups nest. Each parent reflects the state of the group directly below it, and the top parent covers everything.

```tsx title="components/examples/checkbox/nested-groups.tsx"
"use client"

import * as React from "react"

import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"

const userPermissions = ["users.read", "users.write", "users.delete"]
const billingPermissions = ["billing.read", "billing.write"]

export function CheckboxNestedGroups() {
  const [users, setUsers] = React.useState<string[]>(["users.read"])
  const [billing, setBilling] = React.useState<string[]>([])

  return (
    <div className="text-sm">
      <CheckboxGroup
        aria-label="Permissions"
        value={[...users, ...billing]}
        onValueChange={(next) => {
          setUsers(next.filter((value) => value.startsWith("users.")))
          setBilling(next.filter((value) => value.startsWith("billing.")))
        }}
        allValues={[...userPermissions, ...billingPermissions]}
      >
        <label className="flex items-center gap-3">
          <Checkbox parent />
          All permissions
        </label>
        <div className="flex flex-col gap-3 ps-7">
          <CheckboxGroup
            aria-label="Users"
            value={users}
            onValueChange={setUsers}
            allValues={userPermissions}
          >
            <label className="flex items-center gap-3">
              <Checkbox parent />
              Users
            </label>
            <div className="flex flex-col gap-3 ps-7">
              {userPermissions.map((permission) => (
                <label key={permission} className="flex items-center gap-3">
                  <Checkbox value={permission} />
                  {permission}
                </label>
              ))}
            </div>
          </CheckboxGroup>
          <CheckboxGroup
            aria-label="Billing"
            value={billing}
            onValueChange={setBilling}
            allValues={billingPermissions}
          >
            <label className="flex items-center gap-3">
              <Checkbox parent />
              Billing
            </label>
            <div className="flex flex-col gap-3 ps-7">
              {billingPermissions.map((permission) => (
                <label key={permission} className="flex items-center gap-3">
                  <Checkbox value={permission} />
                  {permission}
                </label>
              ))}
            </div>
          </CheckboxGroup>
        </div>
      </CheckboxGroup>
    </div>
  )
}
```

### Controlled

Pass `checked` and `onCheckedChange` to keep the state in your own code.

```tsx title="components/examples/checkbox/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxControlled() {
  const [checked, setChecked] = React.useState(false)

  return (
    <div className="flex flex-col items-start gap-3">
      <label className="flex items-center gap-3 text-sm">
        <Checkbox checked={checked} onCheckedChange={setChecked} />
        Controlled ({checked ? "on" : "off"})
      </label>
      <Button variant="outline" size="sm" onClick={() => setChecked(!checked)}>
        Toggle from outside
      </Button>
    </div>
  )
}
```

### Form

A hidden input submits `name` and `value` like a native checkbox, and `required` blocks submit until it is ticked.

```tsx title="components/examples/checkbox/form.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxForm() {
  const [submitted, setSubmitted] = React.useState<string>()

  return (
    <form
      className="flex flex-col items-start gap-3 text-sm"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(JSON.stringify(Object.fromEntries(data.entries())))
      }}
    >
      <label className="flex items-center gap-3">
        <Checkbox name="terms" required />I agree to the terms
      </label>
      <label className="flex items-center gap-3">
        <Checkbox name="newsletter" value="weekly" defaultChecked />
        Weekly newsletter
      </label>
      <Button type="submit" size="sm">
        Submit
      </Button>
      <output className="text-muted-foreground">
        {submitted ?? "Nothing submitted yet."}
      </output>
    </form>
  )
}
```

### Sibling label

When the label can’t wrap the box, render the checkbox as a `<button>` with `nativeButton` and point the label at it with `htmlFor`.

```tsx title="components/examples/checkbox/sibling-label.tsx"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxSiblingLabel() {
  return (
    <div className="flex items-center gap-2 text-sm">
      <Checkbox id="terms" nativeButton render={<button />} />
      <label htmlFor="terms">Accept terms and conditions</label>
    </div>
  )
}
```

### Cards

Wrap a whole card in the label so the card is the hit area, and style it with `has-data-checked`.

```tsx title="components/examples/checkbox/cards.tsx"
import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"

const addOns = [
  { value: "analytics", title: "Analytics", text: "Dashboards and exports." },
  { value: "backups", title: "Backups", text: "Daily snapshots, 30 days." },
]

export function CheckboxCards() {
  return (
    <CheckboxGroup
      aria-label="Add-ons"
      defaultValue={["analytics"]}
      className="w-full max-w-md"
    >
      <div className="grid gap-3 sm:grid-cols-2">
        {addOns.map((addOn) => (
          <label
            key={addOn.value}
            className="flex items-start gap-3 rounded-xl border p-4 text-sm transition-colors has-data-checked:border-primary"
          >
            <span className="flex h-5 items-center">
              <Checkbox value={addOn.value} />
            </span>
            <span className="flex flex-col gap-0.5">
              <span className="leading-5 font-medium">{addOn.title}</span>
              <span className="text-muted-foreground">{addOn.text}</span>
            </span>
          </label>
        ))}
      </div>
    </CheckboxGroup>
  )
}
```

### Long content

The box stays on the first line while a long label and unbroken text wrap beside it.

```tsx title="components/examples/checkbox/long-content.tsx"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxLongContent() {
  return (
    <label className="flex w-64 max-w-full items-start gap-3 text-sm">
      <span className="flex h-5 items-center">
        <Checkbox />
      </span>
      <span className="flex min-w-0 flex-col gap-0.5 wrap-anywhere">
        <span className="leading-5 font-medium">
          A label long enough to wrap onto a second and even a third line in a
          narrow container
        </span>
        <span className="text-muted-foreground">
          averyveryverylongunbrokenstringthatwouldotherwiseescapeitscontainer
        </span>
      </span>
    </label>
  )
}
```

### Right to left

The box sits on the inline start side and the label follows the reading direction.

```tsx title="components/examples/checkbox/rtl.tsx"
import { Checkbox } from "@/components/ui/checkbox"

export function CheckboxRtl() {
  return (
    <div dir="rtl" className="flex flex-col gap-3 text-sm">
      <label className="flex items-center gap-3">
        <Checkbox defaultChecked />
        تذكر هذا الجهاز
      </label>
      <label className="flex max-w-sm items-start gap-3">
        <span className="flex h-5 items-center">
          <Checkbox />
        </span>
        <span className="flex flex-col gap-0.5">
          <span className="leading-5 font-medium">الإشارات والردود</span>
          <span className="text-muted-foreground">
            سنرسل لك إشعارات فقط حول نشاط منشوراتك.
          </span>
        </span>
      </label>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Space` | Ticks or unticks the checkbox. |
| `Enter` | Submits the form the checkbox belongs to, like a native checkbox. It never toggles the box. |
| `Tab` | Moves focus to the next checkbox. |

## Accessibility

- Wrap the checkbox and its text in a `<label>`. The label names the checkbox, and hovering or pressing it gives the box the same feedback as hovering the box itself.
- Give every `<CheckboxGroup />` an `aria-label` or `aria-labelledby` so screen readers announce what the group is for.
- The hit area extends past the 16px box, and grows on touch screens.
- The check draws in when ticked. With reduced motion it appears without the stroke animation.

## API reference

Built on the Base UI checkbox and checkbox group. Both accept the props of the primitive they wrap.

### Checkbox

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `boolean` | – |  |
| `defaultChecked` | `boolean` | `false` |  |
| `onCheckedChange` | `(checked: boolean, details) => void` | – |  |
| `indeterminate` | `boolean` | `false` | Shows a dash: neither ticked nor unticked. |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` | Focusable, but the value can’t change. |
| `required` | `boolean` | `false` |  |
| `name` | `string` | – | Submitted with the form when ticked. |
| `value` | `string` | – | Identifies the box inside a group and is what the form submits. Falls back to name, then “on”. |
| `uncheckedValue` | `string` | – | Submitted when unticked. Nothing by default. |
| `parent` | `boolean` | `false` | Controls every value in the group’s allValues. Only inside a CheckboxGroup. |
| `inputRef` | `Ref<HTMLInputElement>` | – |  |
| `nativeButton` | `boolean` | `false` | Set to true when render is a <button>. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<span>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="checkbox"` | Target the box in CSS. |
| `data-checked` | Present when ticked. |
| `data-unchecked` | Present when unticked. |
| `data-indeterminate` | Present when indeterminate. |
| `data-disabled` | Present when disabled. |
| `data-readonly` | Present when read-only. |
| `data-required` | Present when required. |
| `data-invalid` | Present when invalid inside a Base UI Field. |

### CheckboxGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string[]` | – | Values of the ticked checkboxes. |
| `defaultValue` | `string[]` | – |  |
| `onValueChange` | `(value: string[], details) => void` | – |  |
| `allValues` | `string[]` | – | Every value in the group. Required for a parent checkbox. |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="checkbox-group"` | Target the group in CSS. |
| `data-disabled` | Present when the group is disabled. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
