Checkbox
Checkbox is the standard binary toggle — a recessed thumb with a filled-gradient checked state that takes the theme accent by default. It renders a real hidden <input type="checkbox"> for form submission, but flips to an ARIA-only mode when you need a tickable element inside a menu, popover row, or any other non-form context.
<Checkbox checked={a} onChange={setA} />
<Checkbox checked={b} onChange={setB} color="green" />
<Checkbox checked={c} onChange={setC} color="purple" size="md" />
<Checkbox checked disabled />
<Checkbox checked readOnly color="red" />
<Checkbox disabled />Usage
import { Checkbox } from '@cladd-ui/react';
<Checkbox checked={agree} onChange={setAgree} />;By default the root is a <label>, so a click anywhere inside the element toggles the hidden input — no extra wiring required. Pair it with text inside the same <label> to extend the clickable target, or pass as="span" / as="div" when the checkbox already lives inside a label-shaped parent (a ListButton with as="label", for instance).
Examples
Sizes
size accepts sm (default) and md. Unlike Button, Chip, and Spinner — which run the full 2xs → 2xl scale — Checkbox ships only the two sizes that actually make sense for a tickable square. sm is the inline default (20px); md (24px) is the right call when the checkbox is the focal point of its row, or when it's nested inside a same-sized md button or list row (see Inside a button or list row below).
<Checkbox size="md" checked />
<Checkbox size="md" />Inside a button or list row
Checkbox's two sizes are nested sizes — they're calibrated to fit inside a same-sized Button or ListButton without crowding the content row. A md checkbox sits a touch smaller than the md button's text, which is the right proportion: the checkbox reads as the indicator for the row, not a co-equal control fighting the label.
Same trick as Spinner and Chip: pass the same size token to both the wrapping control and the checkbox, and the nested-vs-root sizing math is already baked in. Use as="span" on the checkbox so it doesn't render a nested <label> inside the parent label.
<Button as="label" size="md" variant="transparent">
<Checkbox
as="span"
size="md"
checked={agree}
onChange={setAgree}
color="brand"
/>
Subscribe to release notes
</Button>
<Button as="label" size="md" variant="transparent">
<Checkbox
as="span"
size="md"
checked={remember}
onChange={setRemember}
/>
Remember this device
</Button>Colors
color accepts any of the eleven cladd accent tokens — the accent drives the checked-state fill. When unset, the checkbox inherits the theme accent from CladdProvider, so a brand-coloured checkbox doesn't usually need a color prop. Pass one when the checkbox signals severity (red for a destructive opt-in) or sits alongside a non-default control that needs to read as a single unit.
<Checkbox checked color="brand" size="md" />
<Checkbox checked color="brand" />In a list
The canonical multi-select pattern: a List of ListButton as="label" rows, each with a Checkbox in the icon slot. The ListButton already provides the hover overlay, focus ring, and click target — the checkbox is just the indicator. as="label" on the row turns the whole row into a label for the nested checkbox's hidden input, so clicking anywhere in the row toggles it. Use as="span" on the checkbox itself to avoid a nested <label>.
<Surface outline className="w-64 rounded-3xl">
<List>
<ListTitle>Filter by status</ListTitle>
{FILTERS.map((f) => (
<ListButton
key={f.id}
as="label"
icon={
<Checkbox
as="span"
checked={selected.includes(f.id)}
onChange={() => toggle(f.id)}
/>
}
>
{f.label}
</ListButton>
))}
<ListSeparator />
<ListButton onClick={() => setSelected([])}>Clear filters</ListButton>
</List>
</Surface>Inline form
In compact, single-row forms — newsletter signups, quick-create dialogs, search-with-filters bars — a checkbox slots cleanly between an Input and a Button when it's wrapped in a label-shaped button of its own. The Button as="label" gives the checkbox-plus-text pair the same height and hit target as the surrounding controls so the row reads as one continuous strip.
<form
className="flex flex-wrap items-center gap-2"
onSubmit={(e) => e.preventDefault()}
>
<Input
className="w-64"
type="email"
value={email}
onChange={setEmail}
placeholder="you@example.com"
/>
<Button as="label" variant="transparent">
<Checkbox
as="span"
checked={subscribe}
onChange={setSubscribe}
color="brand"
/>
Weekly digest
</Button>
<Button type="submit" color="brand" variant="gradient">
Subscribe
</Button>
</form>Inline in text
For terms-of-service confirmations and similar consent UI, wrap a Checkbox and a block of prose in a single <label>. Drop a small top margin on the checkbox so the square aligns with the first line of text, and let the label do the click-target work — the user can tap anywhere in the paragraph to toggle.
<label className="flex max-w-md cursor-pointer items-start gap-2 leading-relaxed text-cladd-fg-soft">
<Checkbox
as="span"
checked={accepted}
onChange={setAccepted}
className="mt-0.5"
/>
<span>
I agree to the{' '}
<a href="#" className="text-cladd-primary hover:underline">
terms of service
</a>{' '}
and the{' '}
<a href="#" className="text-cladd-primary hover:underline">
privacy policy
</a>
, and consent to receive product updates at the email above.
</span>
</label>States
disabled dims the checkbox and blocks all interaction — read as "not available right now". readOnly blocks toggling without the dim treatment — read as "locked, but the value is real" (e.g. an enforced organisation policy the user can see but not change). required forwards to the native <input> so the browser's form validation will catch unticked submissions.
<label className="flex items-center gap-2">
<Checkbox as="span" checked disabled />
<span className="text-cladd-fg-soft">Disabled, checked</span>
</label>
<label className="flex items-center gap-2">
<Checkbox as="span" disabled />
<span className="text-cladd-fg-soft">Disabled, unchecked</span>
</label>
<label className="flex items-center gap-2">
<Checkbox as="span" checked readOnly color="red" />
<span className="text-cladd-fg-soft">Read-only (locked)</span>
</label>
<label className="flex items-center gap-2">
<Checkbox as="span" required />
<span className="text-cladd-fg-soft">Required</span>
</label>Playground
size, color, and the boolean state toggles compose freely. The sm / md pair covers most cases; reach for md when the checkbox is the focal point of a row or sits inside a md button or list row.
<Checkbox
checked
onChange={setChecked}
size="md"
color="brand"
disabled={false}
readOnly={false}
/>API Reference
| Name: Type | Default | Description |
|---|---|---|
| as: ElementType | 'label' | Polymorphic root element. Defaults to 'label' so a wrapping <label> activates the hidden input on click.Use 'div'/'span' (etc.) when the checkbox lives inside an existing label or needs a non-label container - see hoverable/focusable for how this changes interactivity. |
| checkClassName: string | — | Extra classes for the inner check icon (e.g. to override its color or size). |
| checked: boolean | false | Controlled checked state. Default false. |
| className: string | — | Extra classes for the outer label/element. |
| color: Color | — | Accent color for the checked state. Default: theme accent. |
| disabled: boolean | — | Visually dim the checkbox and disable interaction. |
| focusable: boolean | — | Auto-computed when omitted: true if as === 'label' OR input is true.Drives whether the focus ring ( FocusableLayer) is rendered. Override for non-label, input-less containers that still need a visible keyboard focus state. |
| hoverable: boolean | — | Auto-computed when omitted: true if as === 'label', otherwise false.Override explicitly for custom containers that should still show hover affordances. |
| input: boolean | — | When true (default), renders a hidden native <input type="checkbox"> for form submission and accessibility.When false, the component falls back to ARIA roles (role="checkbox", aria-checked, keyboard Space/Enter toggling) and onChange is fired from the click handler - useful for non-form contexts (e.g. menu items) or custom controlled wrappers. |
| inputId: string | — | id for the hidden <input>. Used to wire an external <label htmlFor> to this checkbox. |
| name: string | — | Native name - used for form submission and to group radio-like checkboxes. |
| onChange: (checked: boolean, event?: ChangeEvent<HTMLInputElement>) => void | — | Fires when the user toggles the checkbox. First arg is the new checked state, second is the raw event (when fired by the hidden <input>). |
| onClick: (e: MouseEvent) => void | — | Fires on click of the root element. Runs before the internal toggle handler. |
| onPointerDown: (e: PointerEvent) => void | — | Fires on pointerdown of the root element. |
| readOnly: boolean | — | Block toggling without the disabled visual treatment - useful for "locked" states. |
| required: boolean | — | Native required - forwarded to the hidden <input> for form validation. |
| size: CheckboxSize | 'sm' | Checkbox size token. Default 'sm'. |
| value: string | — | Native value - submitted with the form when checked. |