HextaUI

Checkbox

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

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>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/checkbox.json

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

import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"
<label className="flex items-center gap-3">
  <Checkbox defaultChecked />
  Remember this device
</label>
CheckboxGroup
└── Checkbox

States

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

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.

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

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

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

Nothing submitted yet.
"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.

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.

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.

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.

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>
  )
}
KeyAction
SpaceTicks or unticks the checkbox.
EnterSubmits the form the checkbox belongs to, like a native checkbox. It never toggles the box.
TabMoves focus to the next checkbox.
  • 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.

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

PropTypeDefault
checked
boolean–
defaultChecked
booleanfalse
onCheckedChange
(checked: boolean, details) => void–
indeterminateShows a dash: neither ticked nor unticked.
booleanfalse
disabled
booleanfalse
readOnlyFocusable, but the value can’t change.
booleanfalse
required
booleanfalse
nameSubmitted with the form when ticked.
string–
valueIdentifies the box inside a group and is what the form submits. Falls back to name, then “on”.
string–
uncheckedValueSubmitted when unticked. Nothing by default.
string–
parentControls every value in the group’s allValues. Only inside a CheckboxGroup.
booleanfalse
inputRef
Ref<HTMLInputElement>–
nativeButtonSet to true when render is a <button>.
booleanfalse
render
ReactElement | (props, state) => ReactElement<span>
AttributeDescription
data-slot="checkbox"Target the box in CSS.
data-checkedPresent when ticked.
data-uncheckedPresent when unticked.
data-indeterminatePresent when indeterminate.
data-disabledPresent when disabled.
data-readonlyPresent when read-only.
data-requiredPresent when required.
data-invalidPresent when invalid inside a Base UI Field.
PropTypeDefault
valueValues of the ticked checkboxes.
string[]–
defaultValue
string[]–
onValueChange
(value: string[], details) => void–
allValuesEvery value in the group. Required for a parent checkbox.
string[]–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="checkbox-group"Target the group in CSS.
data-disabledPresent when the group is disabled.