Inputs
Checkbox
A native checkbox with a crisp custom box, for independent on/off choices and multi-select lists. It supports checked, unchecked and indeterminate, an optional label and description, and a group wrapper that labels a set of options together.
"use client"; import { useState } from "react";import { Checkbox } from "@/components/ui/checkbox"; const events = [ { id: "paid", label: "Invoice paid", description: "When a client settles an invoice." }, { id: "overdue", label: "Invoice overdue", description: "The morning after the due date." }, { id: "comment", label: "New comment", description: "When someone replies on a shared invoice." },]; export function NotificationsChecklist() { const [on, setOn] = useState<string[]>(["paid", "overdue"]); const all = on.length === events.length; const some = on.length > 0 && !all; return ( <div className="mx-auto w-full max-w-sm rounded-ui-lg border border-line bg-surface shadow-ui"> <div className="border-b border-line px-4 py-3"> <Checkbox label="Email me about invoices" checked={all} indeterminate={some} onChange={() => setOn(all ? [] : events.map((e) => e.id))} /> </div> <div className="flex flex-col gap-4 px-4 py-4 pl-11"> {events.map((e) => ( <Checkbox key={e.id} label={e.label} description={e.description} checked={on.includes(e.id)} onChange={(ev) => setOn((cur) => (ev.target.checked ? [...cur, e.id] : cur.filter((x) => x !== e.id)))} /> ))} </div> </div> );}Usage
components/ui/checkbox.tsx
import { Checkbox } from "@/components/ui/checkbox"; export function Example() { return <Checkbox label="Send a copy to my email" defaultChecked />;}Examples
Select all
The parent box turns indeterminate when only some children are checked. Clicking it checks or clears them all.
"use client"; import { useState } from "react";import { Checkbox } from "@/components/ui/checkbox"; const events = [ { id: "paid", label: "Invoice paid", description: "When a client settles an invoice." }, { id: "overdue", label: "Invoice overdue", description: "The morning after the due date." }, { id: "comment", label: "New comment", description: "When someone replies on a shared invoice." },]; export function NotificationsChecklist() { const [on, setOn] = useState<string[]>(["paid", "overdue"]); const all = on.length === events.length; const some = on.length > 0 && !all; return ( <div className="mx-auto w-full max-w-sm rounded-ui-lg border border-line bg-surface shadow-ui"> <div className="border-b border-line px-4 py-3"> <Checkbox label="Email me about invoices" checked={all} indeterminate={some} onChange={() => setOn(all ? [] : events.map((e) => e.id))} /> </div> <div className="flex flex-col gap-4 px-4 py-4 pl-11"> {events.map((e) => ( <Checkbox key={e.id} label={e.label} description={e.description} checked={on.includes(e.id)} onChange={(ev) => setOn((cur) => (ev.target.checked ? [...cur, e.id] : cur.filter((x) => x !== e.id)))} /> ))} </div> </div> );}States
<div className="mx-auto flex w-fit flex-col gap-3"> <Checkbox label="Unchecked" /> <Checkbox label="Checked" defaultChecked /> <Checkbox label="Indeterminate" indeterminate /> <Checkbox label="Disabled" disabled /> <Checkbox label="Disabled and checked" disabled defaultChecked /></div>With description
<div className="mx-auto w-full max-w-sm"> <Checkbox defaultChecked label="Add late fees automatically" description="Applies 1.5% per month to invoices more than 30 days overdue." /></div>Group
CheckboxGroup renders a fieldset and legend, so the question is read before each option.
<div className="mx-auto grid w-full max-w-xl gap-8 sm:grid-cols-2"> <CheckboxGroup label="Export columns" description="Included in the CSV download."> <Checkbox name="cols" value="client" label="Client" defaultChecked /> <Checkbox name="cols" value="amount" label="Amount" defaultChecked /> <Checkbox name="cols" value="due" label="Due date" /> <Checkbox name="cols" value="status" label="Status" defaultChecked /> </CheckboxGroup> <CheckboxGroup label="Payment methods" error="Choose at least one method."> <Checkbox name="pay" value="card" label="Card" invalid /> <Checkbox name="pay" value="bank" label="Bank transfer" invalid /> </CheckboxGroup></div>Horizontal
<div className="mx-auto w-fit"> <CheckboxGroup label="Working days" orientation="horizontal"> <Checkbox label="Mon" defaultChecked /> <Checkbox label="Tue" defaultChecked /> <Checkbox label="Wed" defaultChecked /> <Checkbox label="Thu" defaultChecked /> <Checkbox label="Fri" /> </CheckboxGroup></div>Props
Checkbox
PropTypeDefaultDescription
labelReactNodedefault: —Visible label. Without it, pass aria-label.descriptionReactNodedefault: —Second line under the label (aria-describedby).indeterminatebooleandefault: falseMixed state; announced as “mixed” by screen readers.checked / defaultCheckedbooleandefault: —Controlled or initial state.onChange(e: ChangeEvent) => voiddefault: —Native change event; read e.target.checked.invalidbooleandefault: —Red border and aria-invalid....propsInputHTMLAttributesdefault: —name, value, disabled, required and other native attributes.
CheckboxGroup
PropTypeDefaultDescription
labelrequiredReactNodedefault: —Legend for the set.descriptionReactNodedefault: —Help text under the legend.errorReactNodedefault: —Error message under the options.orientation"vertical" | "horizontal"default: "vertical"Stack or wrap the options.
Accessibility
- A native <input type="checkbox">: Space toggles it, and it takes part in forms with name and value.
- The whole label row is clickable; the box itself is only 16px, the target is the full row.
- indeterminate sets the DOM property, which screen readers announce as “mixed”.
- CheckboxGroup is a <fieldset> with a <legend>, so the group question is announced when focus enters it.
- Checked state is shown by the check mark, not only the fill color, so it reads in every accent and in high contrast mode.