# Checkbox

> Let people turn independent options on or off or select several related choices.

Source: https://www.honestui.com/docs/components/checkbox

```tsx
import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

export function CheckboxDemo() {
  return (
    <Label>
      <Checkbox />
      Accept terms and conditions
    </Label>
  )
}

```

## Overview [#overview]

Use a checkbox when each option stands on its own and any combination of states is valid: preferences, permissions, filters, notification channels, optional add-ons. Because every checkbox toggles independently, people can select none, some, or all of a set without breaking anything.

Use a radio group when exactly one option must be chosen from a visible set — see [Don't do this](#dont-do-this) for what happens when the two get mixed up. Use Switch for a single setting that takes effect immediately rather than as part of a form submission.

`CheckboxGroup` shares one value across several checkboxes and ships in the same file. It is covered under [Examples](#examples) below.

## Anatomy [#anatomy]

A checkbox has an input, a visual indicator, a label, an optional description, and a state. The indicator draws a checkmark when checked and a dash when indeterminate; both are inline SVGs so they inherit text color and scale with the control. The label should make sense read on its own, because screen readers announce checkboxes by their label text alone.

A group holds several checkboxes that answer one question and submits one shared array value. A group needs its own visible label — through `aria-labelledby`, a `Fieldset` legend, or surrounding heading — so people know what the choices are about before reading the first option.

## States [#states]

**Checked / unchecked.** Checked boxes fill with the accent background and drop their border. Unchecked boxes show a bordered surface with a subtle hover state.

**Indeterminate.** Set `indeterminate` to show the dash. The hidden input receives `aria-checked="mixed"`, so screen readers announce it as partially checked rather than silently disagreeing with the visual. Indeterminate is a display state: clicking an indeterminate checkbox checks or unchecks it like any other toggle, so your code decides what happens to the children.

**Disabled.** Disabled items keep their current visual state but dim to half opacity, show a not-allowed cursor, are skipped by keyboard focus, and submit no value. Use them for options that are visible but unavailable in the current context.

**Read-only.** `readOnly` keeps the checkbox in the tab order and submits its value but ignores clicks, with a default cursor and reduced opacity. Prefer disabled for unavailable options; prefer read-only when the value exists and must be submitted but must not change here.

**Invalid.** Setting `aria-invalid="true"` switches the border and checked fill to the danger colors, so validation reaches assistive technology as well as the eye. Pair it with error text that says what to fix.

## Accessibility [#accessibility]

The checkbox renders a span with `role="checkbox"` plus a visually hidden native input, wrapped in a `Label`. Press <kbd>Space</kbd> to toggle; <kbd>Enter</kbd> also activates it because Base UI gives the element button-style key handling. Focus moves with <kbd>Tab</kbd> and shows a visible ring only for keyboard focus (`focus-visible`).

Always wrap the checkbox in a `Label` (or connect one with `htmlFor`). Text sitting next to a checkbox without label association is invisible to screen readers and does not extend the click target. Wrapping in a `Label` makes the whole phrase clickable, which matters more than the control's own size.

The rendered box is 16 px (`size="large"`) or 12 px (\`size="small") — well under the 44 px comfortable touch target. The component adds no coarse-pointer expansion layer itself, so rely on the wrapping label for tap area, and give adjacent labels vertical breathing room in dense lists.

Group checkboxes under a visible question. Each item needs its own label; a parent or select-all checkbox needs text that explains what it covers, because the indeterminate mark alone carries no meaning out of context.

Colors come from `--hui-*` theme tokens, so hover, checked, danger, and disabled surfaces adapt to dark mode automatically. Layout uses logical properties, so nothing needs mirroring for right-to-left locales; keep custom indentation symmetric with logical utilities such as `ms-4`.

Long labels wrap inside the `Label`; they never truncate or push the indicator out of line. For multi-line content, use the card layout shown in [Card style](#card-style) instead of cramming prose into a single label line.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;checkbox&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/checkbox.tsx

```tsx
"use client"

import { Checkbox as CheckboxPrimitive } from "@base-ui-components/react/checkbox"
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui-components/react/checkbox-group"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const checkboxVariants = cva(
  "inline-flex shrink-0 cursor-pointer appearance-none items-center justify-center rounded-[var(--hui-radius-1)] border border-[var(--hui-color-border-base-secondary)] bg-[var(--hui-color-background-base-primary)] outline-none motion-safe:[transition:background-color_var(--hui-duration-fast)_var(--hui-ease-out),border-color_var(--hui-duration-fast)_var(--hui-ease-out),transform_var(--hui-duration-press)_var(--hui-ease-out)] not-data-disabled:hover:border-[var(--hui-color-border-base-focus)] not-data-disabled:hover:bg-[var(--hui-color-background-base-primary-hover)] not-data-disabled:not-data-readonly:active:scale-[var(--hui-scale-pressed-strong)] focus-visible:[outline:var(--hui-focus-ring)] focus-visible:outline-offset-[var(--hui-focus-ring-offset-accent)] data-checked:border-0 data-checked:bg-[var(--hui-color-background-accent-emphasis)] data-checked:hover:bg-[var(--hui-color-background-accent-emphasis-hover)] data-indeterminate:border-0 data-indeterminate:bg-[var(--hui-color-background-neutral-tertiary)] data-indeterminate:hover:bg-[var(--hui-color-background-neutral-secondary)] data-readonly:cursor-default data-readonly:opacity-70 data-invalid:border-[var(--hui-color-border-danger-primary)] data-invalid:data-checked:bg-[var(--hui-color-background-danger-primary)] data-invalid:data-indeterminate:bg-[var(--hui-color-background-danger-primary)] aria-invalid:border-[var(--hui-color-border-danger-primary)] aria-invalid:data-checked:bg-[var(--hui-color-background-danger-primary)] aria-invalid:data-indeterminate:bg-[var(--hui-color-background-danger-primary)] data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:data-checked:border-0 data-disabled:data-checked:bg-[var(--hui-color-background-accent-emphasis)] data-disabled:data-checked:hover:bg-[var(--hui-color-background-accent-emphasis)] data-disabled:data-indeterminate:border-0 data-disabled:data-indeterminate:bg-[var(--hui-color-background-neutral-tertiary)] data-disabled:data-indeterminate:hover:bg-[var(--hui-color-background-neutral-tertiary)]",
  {
    variants: {
      size: {
        large:
          "size-[var(--hui-space-5)] min-h-[var(--hui-space-5)] min-w-[var(--hui-space-5)]",
        small:
          "size-[var(--hui-space-4)] min-h-[var(--hui-space-4)] min-w-[var(--hui-space-4)]",
      },
    },
    defaultVariants: {
      size: "large",
    },
  }
)

type CheckboxProps = CheckboxPrimitive.Root.Props &
  VariantProps<typeof checkboxVariants>

function Checkbox({ className, size, ...props }: CheckboxProps) {
  return (
    <CheckboxPrimitive.Root
      data-slot="checkbox"
      className={cn(checkboxVariants({ size }), className)}
      {...props}
    >
      <CheckboxPrimitive.Indicator
        data-slot="checkbox-indicator"
        className="flex size-full items-center justify-center text-[var(--hui-color-foreground-accent-emphasis)] motion-safe:[transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out),transform_var(--hui-duration-fast)_var(--hui-ease-out)] data-ending-style:scale-80 data-ending-style:opacity-0 data-starting-style:scale-80 data-starting-style:opacity-0 data-unchecked:opacity-0 [&_svg]:size-full"
        render={(props, state) => (
          <span {...props}>
            {state.indeterminate ? (
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="3"
                strokeLinecap="round"
                strokeLinejoin="round"
              >
                <path d="M5.252 12h13.496" />
              </svg>
            ) : (
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="3"
                strokeLinecap="round"
                strokeLinejoin="round"
              >
                <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
              </svg>
            )}
          </span>
        )}
      />
    </CheckboxPrimitive.Root>
  )
}

const checkboxGroupVariants = cva(
  "flex flex-col gap-[var(--hui-space-3)]",
  {
    variants: {
      orientation: {
        vertical: "flex-col",
        horizontal: "flex-row flex-wrap",
      },
    },
    defaultVariants: {
      orientation: "vertical",
    },
  }
)

type CheckboxGroupProps = Omit<CheckboxGroupPrimitive.Props, "orientation"> &
  VariantProps<typeof checkboxGroupVariants>

function CheckboxGroup({
  className,
  orientation,
  ...props
}: CheckboxGroupProps) {
  return (
    <CheckboxGroupPrimitive
      data-slot="checkbox-group"
      className={cn(checkboxGroupVariants({ orientation }), className)}
      {...props}
    />
  )
}

export { Checkbox, CheckboxGroup, checkboxVariants, checkboxGroupVariants }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

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

```tsx
<Checkbox />
```

A bare `<Checkbox />` has no accessible name. Wrap it in a `Label`, or pass `aria-label` when no visible label fits:

```tsx
<Label>
  <Checkbox defaultChecked />
  Email me about new comments
</Label>
```

Use controlled state (`checked` + `onCheckedChange`) only when the application owns the value; `defaultChecked` covers most forms.

## Don't do this [#dont-do-this]

### Choosing one option from many [#choosing-one-option-from-many]

```tsx
// Bad
<Label><Checkbox value="sm" /> Small</Label>
<Label><Checkbox value="md" /> Medium</Label>
<Label><Checkbox value="lg" /> Large</Label>
```

```tsx
// Good
<RadioGroup defaultValue="md">
  <Label><Radio value="sm" /> Small</Label>
  <Label><Radio value="md" /> Medium</Label>
  <Label><Radio value="lg" /> Large</Label>
</RadioGroup>
```

Checkboxes promise independence: people expect to tick two sizes or none at all. When options are mutually exclusive, checkboxes invite invalid combinations and hide the constraint until validation fails. A radio group makes "exactly one" part of the interface instead of an error message.

### Label text that isn't connected [#label-text-that-isnt-connected]

```tsx
// Bad
<div className="flex items-center gap-2">
  <Checkbox />
  <span>Email me about new comments</span>
</div>
```

```tsx
// Good
<Label>
  <Checkbox />
  Email me about new comments
</Label>
```

Unassociated text is decoration as far as assistive technology is concerned: the checkbox is announced as an unnamed "checkbox", and tapping the words does nothing. Wrapping both in a `Label` names the control and turns the entire phrase into the click and touch target.

### Controlled state without a handler [#controlled-state-without-a-handler]

```tsx
// Bad
<Checkbox checked={subscribed} />

// Good
<Checkbox
  checked={subscribed}
  onCheckedChange={(next) => setSubscribed(next)}
/>

// Good, if you don't need app state
<Checkbox defaultChecked={subscribed} />
```

Passing `checked` without `onCheckedChange` creates a control that ignores every click while looking broken to the person using it. If React state is not required, let the checkbox own its state with `defaultChecked` and read the result from form data.

## Examples [#examples]

Examples cover disabled, described, card-style, and form-connected checkboxes, followed by group patterns including parent and nested-parent selection.

For accessible labeling and validation, use `Field` to connect a checkbox with its label, description, and error. See the [Field examples](/docs/components/field#examples).

### Disabled [#disabled]

Both unchecked and pre-checked items stay readable but reject interaction.

```tsx
import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

export function CheckboxDisabledDemo() {
  return (
    <Label>
      <Checkbox defaultChecked disabled />
      Accept terms and conditions
    </Label>
  )
}

```

### With Description [#with-description]

```tsx
import * as React from "react"

import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

export function CheckboxWithDescriptionDemo() {
  const id = React.useId()

  return (
    <div className="flex items-start gap-2">
      <Checkbox id={id} defaultChecked />
      <div className="flex flex-col gap-1">
        <Label htmlFor={id}>Accept terms and conditions</Label>
        <p className="text-xs text-muted-foreground">
          By clicking this checkbox, you agree to the terms and
          conditions.
        </p>
      </div>
    </div>
  )
}

```

### Card Style [#card-style]

The whole card is one `Label`, so the description extends the tap target too.

```tsx
import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

export function CheckboxCardDemo() {
  return (
    <Label className="flex items-start gap-2 rounded-lg border p-3 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50">
      <Checkbox defaultChecked />
      <div className="flex flex-col gap-1">
        <p className="text-sm leading-4">Enable notifications</p>
        <p className="text-xs text-muted-foreground">
          You can enable or disable notifications at any time.
        </p>
      </div>
    </Label>
  )
}

```

### Form Integration [#form-integration]

Field provides accessible labeling and validation structure for form controls. Use it with `Form` to submit values.

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import { Checkbox } from "@/components/honest-ui/ui/checkbox";
import { Field, FieldLabel } from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";

export function CheckboxFormDemo() {
  const [status, setStatus] = React.useState("");
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    setStatus(
      formData.has("terms")
        ? "Submitted: terms accepted."
        : "Submitted: terms not accepted.",
    );
  };
  return (
    <Form onSubmit={onSubmit} className="grid w-auto gap-4">
      <Field name="terms">
        <FieldLabel>
          <Checkbox name="terms" value="yes" defaultChecked />
          Accept terms and conditions
        </FieldLabel>
      </Field>
      <Button type="submit">Accept terms</Button>
      <p className="text-sm text-muted-foreground" role="status">
        {status}
      </p>
    </Form>
  );
}

```

## Checkbox Group [#checkbox-group]

Checkbox Group is included in the same component file. It manages a single `string[]` value across its items and lays them out vertically by default; `orientation="horizontal"` wraps items in a row. Pass `allValues` to enable a `parent` checkbox that selects and clears every item, with the indeterminate mark appearing automatically while only some children are selected.

### Basic Group [#basic-group]

```tsx
import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { CheckboxGroup } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

export function CheckboxGroupDemo() {
  return (
    <CheckboxGroup
      aria-label="Select frameworks"
      defaultValue={["next"]}
    >
      <Label>
        <Checkbox value="next" />
        Next.js
      </Label>
      <Label>
        <Checkbox value="vite" />
        Vite
      </Label>
      <Label>
        <Checkbox value="astro" />
        Astro
      </Label>
    </CheckboxGroup>
  )
}

```

### With Disabled Item [#with-disabled-item]

```tsx
import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { CheckboxGroup } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

export function CheckboxGroupDemo() {
  return (
    <CheckboxGroup
      aria-label="Select frameworks"
      defaultValue={["next"]}
    >
      <Label>
        <Checkbox value="next" />
        Next.js
      </Label>
      <Label>
        <Checkbox value="vite" disabled />
        Vite
      </Label>
      <Label>
        <Checkbox value="astro" />
        Astro
      </Label>
    </CheckboxGroup>
  )
}

```

### Parent Checkbox [#parent-checkbox]

The parent selects all children and reports partial selection with the indeterminate dash.

```tsx
"use client"

import * as React from "react"

import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { CheckboxGroup } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

const frameworks = [
  { id: "next", name: "Next.js" },
  { id: "vite", name: "Vite" },
  { id: "astro", name: "Astro" },
]

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

  return (
    <CheckboxGroup
      aria-labelledby="frameworks-caption"
      value={value}
      onValueChange={setValue}
      allValues={frameworks.map((framework) => framework.id)}
    >
      <Label id="frameworks-caption">
        <Checkbox name="frameworks" parent />
        Frameworks
      </Label>

      {frameworks.map((framework) => (
        <Label key={framework.id} className="ms-4">
          <Checkbox value={framework.id} />
          {framework.name}
        </Label>
      ))}
    </CheckboxGroup>
  )
}

```

### Nested Parent Checkbox [#nested-parent-checkbox]

Parent groups can contain child groups for hierarchical permissions.

```tsx
"use client"

import * as React from "react"

import { Checkbox } from "@/components/honest-ui/ui/checkbox"
import { CheckboxGroup } from "@/components/honest-ui/ui/checkbox"
import { Label } from "@/components/honest-ui/ui/label"

const mainPermissions = [
  { id: "view-dashboard", name: "View Dashboard" },
  { id: "manage-users", name: "Manage Users" },
  { id: "access-reports", name: "Access Reports" },
]

const userManagementPermissions = [
  { id: "create-user", name: "Create User" },
  { id: "edit-user", name: "Edit User" },
  { id: "delete-user", name: "Delete User" },
  { id: "assign-roles", name: "Assign Roles" },
]

export function CheckboxGroupNestedParentDemo() {
  const [mainValue, setMainValue] = React.useState<string[]>([])
  const [managementValue, setManagementValue] = React.useState<string[]>([])

  const managementIsPartial =
    managementValue.length > 0 &&
    managementValue.length !== userManagementPermissions.length

  return (
    <CheckboxGroup
      aria-labelledby="user-permissions-caption"
      value={mainValue}
      onValueChange={(value) => {
        if (value.includes("manage-users")) {
          setManagementValue(userManagementPermissions.map((p) => p.id))
        } else if (
          managementValue.length === userManagementPermissions.length
        ) {
          setManagementValue([])
        }
        setMainValue(value)
      }}
      allValues={mainPermissions.map((p) => p.id)}
    >
      <Label id="user-permissions-caption">
        <Checkbox parent indeterminate={managementIsPartial} />
        User Permissions
      </Label>

      {mainPermissions
        .filter((p) => p.id !== "manage-users")
        .map((p) => (
          <Label key={p.id} className="ms-4">
            <Checkbox value={p.id} />
            {p.name}
          </Label>
        ))}

      <CheckboxGroup
        aria-labelledby="manage-users-caption"
        value={managementValue}
        onValueChange={(value) => {
          if (value.length === userManagementPermissions.length) {
            setMainValue((prev) =>
              Array.from(new Set([...prev, "manage-users"]))
            )
          } else {
            setMainValue((prev) => prev.filter((v) => v !== "manage-users"))
          }
          setManagementValue(value)
        }}
        allValues={userManagementPermissions.map((p) => p.id)}
        className="ms-4"
      >
        <Label id="manage-users-caption">
          <Checkbox parent />
          Manage Users
        </Label>

        {userManagementPermissions.map((p) => (
          <Label key={p.id} className="ms-4">
            <Checkbox value={p.id} />
            {p.name}
          </Label>
        ))}
      </CheckboxGroup>
    </CheckboxGroup>
  )
}

```

### Group Form Integration [#group-form-integration]

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import { Checkbox } from "@/components/honest-ui/ui/checkbox";
import { CheckboxGroup } from "@/components/honest-ui/ui/checkbox";
import { Field, FieldLabel } from "@/components/honest-ui/ui/field";
import { Fieldset, FieldsetLegend } from "@/components/honest-ui/ui/fieldset";
import { Form } from "@/components/honest-ui/ui/form";

export function CheckboxGroupFormDemo() {
  const [status, setStatus] = React.useState("");
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const frameworks = formData.getAll("frameworks") as string[];
    setStatus(`Submitted: ${frameworks.join(", ") || "no frameworks"}.`);
  };

  return (
    <Form onSubmit={onSubmit} className="grid max-w-[160px] gap-4">
      <Field
        name="frameworks"
        className="gap-4"
        render={(props) => <Fieldset {...props} />}
      >
        <FieldsetLegend className="text-sm font-medium">
          Frameworks
        </FieldsetLegend>
        <CheckboxGroup defaultValue={["next"]}>
          <FieldLabel>
            <Checkbox value="next" />
            Next.js
          </FieldLabel>
          <FieldLabel>
            <Checkbox value="vite" />
            Vite
          </FieldLabel>
          <FieldLabel>
            <Checkbox value="astro" />
            Astro
          </FieldLabel>
        </CheckboxGroup>
      </Field>
      <Button type="submit">Save frameworks</Button>
      <p className="text-sm text-muted-foreground" role="status">
        {status}
      </p>
    </Form>
  );
}

```

## API reference [#api-reference]

`Checkbox` and `CheckboxGroup` forward their matching Base UI props and add Honest UI styling:

| Component       | Prop          | Values                   | Default    |
| --------------- | ------------- | ------------------------ | ---------- |
| `Checkbox`      | `size`        | `large`, `small`         | `large`    |
| `CheckboxGroup` | `orientation` | `vertical`, `horizontal` | `vertical` |

On `Checkbox`: use `checked`/`onCheckedChange` or `defaultChecked` for state, `indeterminate` for the mixed visual, `parent` for select-all behavior inside a group, and `name`/`value`/`uncheckedValue` for form submission. An unchecked checkbox submits nothing unless you set `uncheckedValue`. `disabled` removes the item from interaction entirely; `readOnly` blocks changes while keeping the value submittable.

On `CheckboxGroup`: `value`/`onValueChange` hold the selected id array, `defaultValue` seeds it uncontrolled, and `allValues` defines the full set used by parent checkboxes.

See the [Base UI Checkbox API](https://base-ui.com/react/components/checkbox#api-reference) and [Checkbox Group API](https://base-ui.com/react/components/checkbox-group#api-reference).
