# Checkbox

A control that lets the user toggle between checked and not checked.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { HStack, VStack } from "@/components/ui/stack"
import { color, leading, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  group: { width: "100%", maxWidth: "24rem" },
  description: {
    color: color.mutedForeground,
    fontSize: text.sm,
    lineHeight: leading.sm,
    fontWeight: 400,
  },
})

export default function CheckboxDemo() {
  return (
    <VStack gap={6} xstyle={styles.group}>
      <HStack gap={2} align="center">
        <Checkbox id="terms-checkbox" name="terms-checkbox" />
        <Label htmlFor="terms-checkbox">Accept terms and conditions</Label>
      </HStack>
      <HStack gap={2} align="start">
        <Checkbox id="terms-checkbox-2" name="terms-checkbox-2" defaultChecked />
        <VStack gap={1}>
          <Label htmlFor="terms-checkbox-2">Accept terms and conditions</Label>
          <span {...stylex.props(styles.description)}>
            By clicking this checkbox, you agree to the terms.
          </span>
        </VStack>
      </HStack>
      <HStack gap={2} align="center">
        <Checkbox id="toggle-checkbox" name="toggle-checkbox" disabled />
        <Label htmlFor="toggle-checkbox" disabled>
          Enable notifications
        </Label>
      </HStack>
      <Label>
        <Checkbox name="toggle-checkbox-2" />
        <VStack gap={1}>
          <span>Enable notifications</span>
          <span {...stylex.props(styles.description)}>
            You can enable or disable notifications at any time.
          </span>
        </VStack>
      </Label>
    </VStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and
   `state-styles.ts` from the same page. The checkbox imports both. It also needs
   `interop.css`, which sizes the tick. The
   examples caption it with a [Label](https://carbon.style/docs/components/label).

   Paste `field-state.ts` from the same installation page.

3. Copy and paste the following code into your project.

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

   import { use, type CSSProperties } from "react"
   import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { Check, Minus } from "lucide-react"

   import { stateStyles } from "@/lib/state-styles"
   import { FieldStateContext } from "@/lib/field-state"
   import { states } from "@/theme/states"
   import { color, duration, ease, radius, space } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     root: {
       position: "relative",
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
       flexShrink: 0,
       width: space.s4,
       height: space.s4,
       borderRadius: radius.sm,
       borderWidth: 1,
       borderStyle: "solid",
       transitionProperty: "color, background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       "--carbon-icon-size": space.s3_5,
       "::after": {
         content: '""',
         position: "absolute",
         insetInline: `calc(-1 * ${space.s3})`,
         insetBlock: `calc(-1 * ${space.s2})`,
       },
     },
     checked: {
       "--carbon-border-color": color.primary,
       backgroundColor: color.primary,
       color: color.primaryForeground,
     },
     indicator: {
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
       transitionProperty: "opacity, transform",
       transitionDuration: {
         default: duration.color,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
     indicatorHidden: { opacity: 0, transform: "scale(0.6)" },
   })

   const checkboxStyles = {
     root: styles.root,
     choice: states.keyChoice,
     checked: styles.checked,
     disabled: states.disabled,
     invalid: states.invalidRing,
   }

   type CheckboxProps = Omit<CheckboxPrimitive.Root.Props, "className" | "style"> & {
     xstyle?: StyleXStyles
     style?: CSSProperties
     className?: string
   }

   function Checkbox({ xstyle, style, className, indeterminate, ...props }: CheckboxProps) {
     const fieldState = use(FieldStateContext)
     return (
       <CheckboxPrimitive.Root
         data-slot="checkbox"
         indeterminate={indeterminate}
         {...props}
         {...stateStyles(
           (state) => [
             styles.root,
             states.keyChoice,
             states.focusRing,
             (state.valid === false ||
               fieldState?.valid === false ||
               props["aria-invalid"] === true ||
               props["aria-invalid"] === "true") &&
               states.invalidRing,
             (state.checked || state.indeterminate) && styles.checked,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       >
         <CheckboxPrimitive.Indicator
           data-slot="checkbox-indicator"
           {...stateStyles((state) => [
             styles.indicator,
             (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
               styles.indicatorHidden,
           ])}
           render={(indicatorProps, state) => (
             <span {...indicatorProps}>{state.indeterminate ? <Minus /> : <Check />}</span>
           )}
         />
       </CheckboxPrimitive.Root>
     )
   }

   export { Checkbox, checkboxStyles }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Checkbox id="terms" />
<Label htmlFor="terms">Accept terms and conditions</Label>
```

Lay a checkbox and its label out in an `HStack`, or in a horizontal
[Field](https://carbon.style/docs/components/field) for automatic label association.

## Checked state

Use `defaultChecked` for an uncontrolled checkbox, or `checked` and
`onCheckedChange` to control it.

```tsx
import { useState } from "react"

export function Example() {
  const [checked, setChecked] = useState(false)

  return <Checkbox checked={checked} onCheckedChange={setChecked} />
}
```

## Invalid state

Set `invalid` on `Field` or `aria-invalid` on a bare checkbox. Both show the destructive border and ring.

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldLabel, FieldError } from "@/components/ui/field"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

export default function CheckboxInvalid() {
  return (
    <VStack gap={5} width="100%" maxWidth="24rem">
      <VStack gap={2}>
        <Label htmlFor="checkbox-invalid-bare">Accept terms and conditions</Label>
        <Checkbox
          id="checkbox-invalid-bare"
          aria-invalid
          aria-describedby="checkbox-invalid-bare-error"
        />
        <FieldError id="checkbox-invalid-bare-error">
          You must accept the terms to continue.
        </FieldError>
      </VStack>
      <Field invalid>
        <FieldLabel>Accept terms and conditions</FieldLabel>
        <Checkbox />
        <FieldError>You must accept the terms to continue.</FieldError>
      </Field>
    </VStack>
  )
}
```

## Basic

Pair the checkbox with a `Label` in an `HStack`.

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { HStack } from "@/components/ui/stack"

export default function CheckboxBasic() {
  return (
    <HStack gap={2} align="center">
      <Checkbox id="terms-checkbox-basic" name="terms-checkbox-basic" />
      <Label htmlFor="terms-checkbox-basic">Accept terms and conditions</Label>
    </HStack>
  )
}
```

## Description

Stack the label and a line of helper text in a `VStack` beside the checkbox,
and align the row to `start` so the box sits on the first line.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { HStack, VStack } from "@/components/ui/stack"
import { color, leading, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "18rem" },
  description: { color: color.mutedForeground, fontSize: text.sm, lineHeight: leading.sm },
})

export default function CheckboxDescription() {
  return (
    <HStack gap={2} align="start" xstyle={styles.field}>
      <Checkbox id="terms-checkbox-desc" name="terms-checkbox-desc" defaultChecked />
      <VStack gap={1}>
        <Label htmlFor="terms-checkbox-desc">Accept terms and conditions</Label>
        <span {...stylex.props(styles.description)}>
          By clicking this checkbox, you agree to the terms and conditions.
        </span>
      </VStack>
    </HStack>
  )
}
```

## Disabled

Set `disabled` on `Field` to disable its control and dim its label, or on a bare control directly.

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldLabel } from "@/components/ui/field"
export default function CheckboxDisabled() {
  return (
    <Field orientation="horizontal" disabled>
      <Checkbox name="notifications" />
      <FieldLabel>Enable notifications</FieldLabel>
    </Field>
  )
}
```

## Group

Use [Checkbox Group](https://carbon.style/docs/components/checkbox-group) to share selection state and Field validation.

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Field, FieldDescription, FieldItem, FieldLabel } from "@/components/ui/field"
const items = [
  { value: "hard-disks", label: "Hard disks" },
  { value: "external-disks", label: "External disks" },
  { value: "cds-dvds", label: "CDs, DVDs, and iPods" },
  { value: "connected-servers", label: "Connected servers" },
]
export default function CheckboxGroupExample() {
  return (
    <Field name="desktop-items">
      <FieldLabel nativeLabel={false} render={<div />}>
        Show these items on the desktop:
      </FieldLabel>
      <FieldDescription>Select the items you want to show on the desktop.</FieldDescription>
      <CheckboxGroup defaultValue={["hard-disks", "external-disks"]}>
        {items.map(({ value, label }) => (
          <FieldItem key={value}>
            <FieldLabel>
              <Checkbox value={value} />
              {label}
            </FieldLabel>
          </FieldItem>
        ))}
      </CheckboxGroup>
    </Field>
  )
}
```

## Indeterminate

Use `parent` inside a controlled [Checkbox Group](https://carbon.style/docs/components/checkbox-group) with `allValues` to derive the checked and indeterminate states automatically.

Use the `indeterminate` prop for a parent that controls a set of children. It
draws a dash in place of the tick and is independent of `checked`.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { HStack, VStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  children: { paddingInlineStart: space.s6 },
})

const permissions = [
  { id: "read", label: "Read" },
  { id: "write", label: "Write" },
  { id: "deploy", label: "Deploy" },
]

export default function CheckboxIndeterminate() {
  const [granted, setGranted] = useState<string[]>(["read"])

  const all = granted.length === permissions.length
  const some = granted.length > 0 && !all

  return (
    <VStack gap={3}>
      <HStack gap={2} align="center">
        <Checkbox
          id="permissions-all"
          checked={all}
          indeterminate={some}
          onCheckedChange={(checked) => setGranted(checked ? permissions.map((p) => p.id) : [])}
        />
        <Label htmlFor="permissions-all">All permissions</Label>
      </HStack>
      <VStack gap={3} xstyle={styles.children}>
        {permissions.map((permission) => (
          <HStack key={permission.id} gap={2} align="center">
            <Checkbox
              id={`permissions-${permission.id}`}
              checked={granted.includes(permission.id)}
              onCheckedChange={(checked) =>
                setGranted((current) =>
                  checked
                    ? [...current, permission.id]
                    : current.filter((id) => id !== permission.id),
                )
              }
            />
            <Label htmlFor={`permissions-${permission.id}`}>{permission.label}</Label>
          </HStack>
        ))}
      </VStack>
    </VStack>
  )
}
```

> **Note**
>
> `indeterminate` is a display state. Clicking the checkbox still fires
> `onCheckedChange` with the opposite of `checked`, so decide what a click on a
> mixed parent means and set `checked` from that.

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Checkbox` wraps the Base UI `Checkbox.Root` and renders its `Indicator`
inside. See the [Base UI reference](https://base-ui.com/react/components/checkbox)
for the full list of props.

### Checkbox

| Prop              | Type                                  | Default |
| ----------------- | ------------------------------------- | ------- |
| `checked`         | `boolean`                             | —       |
| `defaultChecked`  | `boolean`                             | `false` |
| `onCheckedChange` | `(checked: boolean, details) => void` | —       |
| `indeterminate`   | `boolean`                             | `false` |
| `disabled`        | `boolean`                             | `false` |
| `readOnly`        | `boolean`                             | `false` |
| `required`        | `boolean`                             | `false` |
| `name`            | `string`                              | —       |
| `value`           | `string`                              | —       |
| `id`              | `string`                              | —       |
| `inputRef`        | `Ref<HTMLInputElement>`               | —       |
| `xstyle`          | `StyleXStyles`                        | —       |
| `style`           | `CSSProperties`                       | —       |
| `className`       | `string`                              | —       |

The root is a `<span role="checkbox">`. Base UI renders a hidden `<input>`
beside it that carries `id`, `name` and `value`. A label's `htmlFor` and a
form submission reach the control through that input.

### checkboxStyles

The style maps behind the component. Spread them onto any element.

| Key        | What it is                                     |
| ---------- | ---------------------------------------------- |
| `root`     | The box: size, border width, corner, hit area  |
| `choice`   | The hairline edge and fill at a choice's size  |
| `checked`  | The flat primary fill and inverted tick colour |
| `disabled` | The dimmed, non-interactive state              |
| `invalid`  | The destructive border and ring                |

`root` carries no colour and no focus ring. Everything else comes from `states`
in `theme/states.ts`. Merge `choice`, then `states.focusRing`. Merge `invalid`
before `checked`, so a ticked invalid box keeps the primary fill and shows only
the destructive ring.

### Data attributes

| Attribute                                         | On              | Effect                                              |
| ------------------------------------------------- | --------------- | --------------------------------------------------- |
| `data-slot="checkbox"`                            | the root        | Targets the box from a parent                       |
| `data-slot="checkbox-indicator"`                  | the indicator   | Targets the animated indicator                      |
| `data-checked`, `data-unchecked`                  | root, indicator | Base UI writes them                                 |
| `data-parent`                                     | root            | Controls the group selection                        |
| `data-indeterminate`                              | root, indicator | Base UI writes it                                   |
| `data-disabled`, `data-readonly`, `data-required` | root, indicator | Base UI writes them                                 |
| `data-starting-style`, `data-ending-style`        | the indicator   | Base UI writes them while the tick enters or leaves |
