Checkbox
Checkboxes let users select one or more items from a set, or toggle an option on or off. Use them for multiple independent choices; for a single immediate setting, use a switch instead.
The Checkbox wrapper renders an <a-checkbox> element that manages its own state. It works both controlled and uncontrolled with checked / defaultChecked (boolean or 'indeterminate') and onStateChange for updates.
Playground
Tone
tone colors the mark — the checked-box fill and, for a tinted tone, the
unselected box border (a light shade of the fill that darkens on hover and active,
like the neutral border; neutral keeps a grey border). The label and hint stay
neutral. Six named tones (neutral default), or pass any CSS color for a custom tone.
<Checkbox defaultChecked tone="neutral">Neutral</Checkbox><Checkbox defaultChecked tone="brand">Brand</Checkbox><Checkbox defaultChecked tone="critical">Critical</Checkbox><Checkbox defaultChecked tone="mediumaquamarine">Custom color</Checkbox>Selected-only tone
toneScope="selected" applies the checkbox’s tone to the checked mark only —
the empty box stays neutral grey until it’s checked. Use it when a resting tinted
border would read as a validation error.
<Checkbox tone="brand" toneScope="selected">Off stays neutral</Checkbox><Checkbox defaultChecked tone="critical" toneScope="selected">Checked mark tinted</Checkbox>Coloring the text
There’s no text-tone prop. To tint the label, set color on the checkbox with a
theme-aware --text-N-{tone} token (the label inherits it); for the hint, add a
color rule on the a-checkbox-hint child. Pair either with tone
to tint the whole control.
/* Mark via the prop, text via a plain color rule — the label inherits the host color, the hint is targeted directly. The class is just for the demo — swap your own selector. */a-checkbox.toned-text { color: var(--text-1-critical); }a-checkbox.toned-text a-checkbox-hint { color: var(--text-3-critical); }Size
Three sizes scale the box and the label + hint type together: small
(14px box / 13px label), medium (16px / 15px, the default), and large
(18px / 17px).
<Checkbox defaultChecked size="small">Small</Checkbox><Checkbox defaultChecked>Medium</Checkbox><Checkbox defaultChecked size="large">Large</Checkbox>Label and hint
Pass the label as children or the label prop. Add a hint for a secondary line beneath. For a label-less checkbox (e.g. a table “select all”), pass aria-label.
<Checkbox label="Email notifications" hint="Weekly digest, never marketing." /><Checkbox aria-label="Select all rows" />States
Checkboxes are unchecked, checked, or indeterminate (for partial selection in groups).
<Checkbox defaultChecked label="Checked" /><Checkbox label="Unchecked" /><Checkbox defaultChecked="indeterminate" label="Indeterminate" />Disabled
The disabled attribute dims the checkbox and removes it from the tab order.
<Checkbox disabled label="Disabled" /><Checkbox disabled defaultChecked label="Disabled checked" />Controlled
Uncontrolled by default: pass defaultChecked and read the value from onStateChange or at form-submit. Make it controlled with checked and onStateChange to drive it from your store.
const [agreed, setAgreed] = useState(false)
<Checkbox checked={agreed} onStateChange={(_e, { next }) => setAgreed(next as boolean)}> I agree</Checkbox>onStateChange fires before the element applies, with signature (event, { next, prev }). Call event.preventDefault() to veto the change. A controlled checkbox with no onStateChange is read-only.
Events
Three callbacks, in firing order:
| Callback | When | Cancelable | Payload |
|---|---|---|---|
onStateChange(e, { next, prev }) | before the toggle applies | ✅ e.preventDefault() vetoes | next / prev state |
onChange(e) | after it applies | — | native change event |
onValueChange(e, attrs) | after it applies | — | { checked, indeterminate, name, value } |
Reach for onStateChange only to intercept or veto (or to drive a controlled
checkbox). For the everyday “react to the new value”, use onValueChange — it
hands you the value without touching event.target. onChange is the plain
native hook. (In controlled mode onChange/onValueChange fire once you’ve updated
checked.) onFocus / onBlur work too — the host is the focusable element.
Checkbox has onStateChange (and Input doesn’t) because its state is
discrete — checked · unchecked · indeterminate — so a transition can be
vetoed before it applies. A text field’s value is free-form, with no discrete
state to model or veto, so Input (and the composed Select) expose only the
onValueChange half. The onValueChange shape is identical across all of them.
<Checkbox onValueChange={(_e, { checked }) => save(checked)}>Subscribe</Checkbox>Indeterminate
Set checked="indeterminate" to show the “mixed” state (minus glyph). Common for a parent checkbox whose state derives from its children: all on = checked, all off = unchecked, mixed = indeterminate.
function ParentChild() { const [on, setOn] = useState([true, false]) return ( <> <Checkbox checked={on[0] && on[1] ? true : on[0] || on[1] ? 'indeterminate' : false} onStateChange={(_e, { next }) => setOn([next === true, next === true])} > Parent </Checkbox> <div style={{ marginInlineStart: 24 }}> <Checkbox checked={on[0]} onStateChange={(_e, { next }) => setOn([next === true, on[1]])} > Child 1 </Checkbox> <Checkbox checked={on[1]} onStateChange={(_e, { next }) => setOn([on[0], next === true])} > Child 2 </Checkbox> </div> </> )}Accessibility
The wrapper sets role="checkbox", and the element keeps aria-checked
("true" / "false" / "mixed") in sync. Its light-DOM label supplies the
accessible name and its hint supplies the accessible description through
ElementInternals; no generated IDs are needed. For a label-less checkbox, pass
aria-label.
Space toggles the checkbox. Follows the WAI-ARIA checkbox pattern.
Component props
| Prop | Type | Default | Description |
|---|---|---|---|
| label? | string | — | Visible label — the value of the checkbox (clicked along with the box).
Convenience for the common single-string case; for richer content (markup,
a link, an info icon) use children. When both are supplied, label
renders first. Required unless children or aria-label is provided
(a role="checkbox" takes its name from the author, not the markup). |
| hint? | ReactNode | — | Secondary text rendered under the label and exposed as the checkbox's accessible description. Not part of the accessible name. |
| checked? | CheckboxValue | — | Controlled checked state. When provided the checkbox is controlled — it
renders exactly this and never self-applies; onStateChange is a request
the consumer accepts by updating this prop. Use defaultChecked for an
uncontrolled checkbox. 'indeterminate' shows the minus glyph and takes
visual precedence; clicking it requests true. |
| defaultChecked? | CheckboxValue | false | Initial checked state for an uncontrolled checkbox. Read once; later changes are ignored and the element updates its state after interaction. |
| disabled? | boolean | — | Disable the checkbox (no interaction, dropped from the tab order). |
| round? | booleannumberstring | — | Round the checkbox mark to a circle (border-radius: 999px on the box). Pass
a number (px) or a CSS length string for a rounded-square mark instead. |
| name? | string | — | Form field name. Inside a <form> the checkbox submits under this name,
contributing value when checked — like a native checkbox. |
| value? | string | "on" | Value submitted with the form when checked — like a native checkbox. |
| tone? | neutralbrandcriticalinfosuccesswarningstring | 'neutral' | Color of the mark. In the default all scope this colors the checked-box
fill and unselected box border. A named tone or any literal CSS color ('#ff1493',
'rebeccapurple') for a one-off custom tone. Named tones track light/dark mode
automatically; a custom color keeps its hue + chroma and pins lightness to the
fill curve. Set toneScope="selected" to tone only the checked mark and leave
the empty box neutral. The label + hint stay neutral — recolor them in plain
CSS via the theme-aware --text-N-{tone} tokens. |
| toneScope? | allselected | 'all' | Apply tone to every state, or only while checked so the empty box stays
neutral. selected is useful when a tinted resting border would read as a
validation state. |
| size? | smallmediumlarge | 'medium' | Size variant. small=14px, medium=16px, large=18px box. |
| onStateChange? | (event, detail) => void | — | Fired on click / Space before the element applies any change. Event-first
so event.preventDefault() is the synchronous veto (uncontrolled mode);
detail carries { next, prev }. In controlled mode the element never
self-applies — answer by updating checked, reject by doing nothing. |
| onChange? | (event) => void | — | Fired after the checked state changes — a native change event (the
post-apply counterpart to onStateChange). Not cancelable. For a controlled
checkbox this fires once you've updated checked. |
| onValueChange? | (event, attrs) => void | — | Like onChange, but with a { checked, indeterminate, name, value } snapshot
as the second argument, matching Input's onValueChange. |
Inherited props (children, className, id, slot, style, tabIndex, title)
| Prop | Type | Default | Description |
|---|---|---|---|
| children? | ReactNode | — | Child elements. When provided, replaces the component's default label/content. |
| className? | string | — | CSS class on the component's root element (merged with the component's own
classes). Use it directly for layout and positioning — grid/flex placement,
margins, alignment — rather than wrapping the component in a <div>/<span>. |
| id? | string | — | HTML id attribute. |
| slot? | string | — | Assigns the element to a named <slot> of a parent web component (e.g.
slot="header" inside a <Card>, slot="footer" inside a <Dialog>). |
| style? | CSSProperties | — | Inline styles on the component's root element. Set layout/positioning here
(or via className) directly on the component instead of adding a wrapper. |
| tabIndex? | number | — | Tab order. Set to -1 to skip the element when tabbing. |
| title? | string | — | HTML title attribute — native browser tooltip on hover. |
Plus every standard DOM event handler (onClick,
onFocus, onKeyDown, …) and any
data-* or aria-* attribute — forwarded as-is to
the underlying <a-*> element.
Web Component
Use the web component directly when you are not using React or Preact and a native control does not fit.
The focusable host carries the state. Label and hint are light-DOM children that the element exposes as its accessible name and description.
<a-checkbox role="checkbox" tabindex="0" name="updates" default-state="checked"> <a-checkbox-label>Email updates</a-checkbox-label> <a-checkbox-hint>Product and security notices.</a-checkbox-hint></a-checkbox>Native HTML checkbox
For a standard HTML form, add data-anta to a native checkbox. It keeps the
browser’s label behavior, checked and indeterminate states, and form submission
while using Anta’s control surface.
size, tone, tone-scope, and round use the matching Checkbox visual
treatments.
<div style="display: grid; gap: 8px"> <label><input data-anta type="checkbox" name="updates" size="small" tone="brand"> Product updates</label> <label><input data-anta type="checkbox" name="security" tone="success" tone-scope="selected" checked> Security notices</label> <label><input data-anta type="checkbox" name="billing" size="large" round tone="warning" checked> Billing alerts</label></div> Styling
Reach for the props first: tone chooses the mark color and toneScope
chooses whether it applies in all states or only while selected (any CSS color
derives the full rest/hover/active curve in oklch); size controls dimensions + type.
To tint the label + hint, add a color rule (on the host for the label, on
a-checkbox-hint for the hint) with the --text-N-{tone} tokens — there’s no
text-tone prop. The focus ring is the global
--focus-ring.
<Checkbox tone="#e0457b" defaultChecked>Custom color</Checkbox>For anything else, <a-checkbox> is light-DOM — the box is ::before, the
check/minus glyph is ::after (a masked SVG — swap --checkbox-mask-check for a
custom mark), the label/hint are the a-checkbox-label / a-checkbox-hint children.
Target them with plain CSS, per state (an un-layered rule beats @layer anta
without !important). The classes below are just for the demos:
/* Outlined — an unfilled box: the checked box keeps its background and paints the border + checkmark in the tone color (light-dark() tunes both themes at once). */a-checkbox.outlined:state(checked)::before { background: var(--checkbox-bg); border-color: light-dark(#5f4bc3, #7460d7);}a-checkbox.outlined:state(checked)::after { background-color: light-dark(#5f4bc3, #7460d7);}
/* Customized — bold label, bigger circular box, bigger checkmark. Colors use light-dark() so they're tuned for both themes at once. */a-checkbox.customized { font-weight: 700; }a-checkbox.customized::before,a-checkbox.customized::after { inline-size: 24px; block-size: 24px; margin-block-start: -2px; }a-checkbox.customized::before { border-radius: 50%; border-width: 2px; }a-checkbox.customized::after { mask-size: 16px; } /* bigger mark */a-checkbox.customized:state(checked)::before { background: light-dark(#1f6e5f, #2f9c84); border-color: light-dark(#1f6e5f, #2f9c84);}
/* Beautified — a full white heart mark swapped in via --checkbox-mask-check (centered by the default mask-position), sized down a touch, on a pink fill */a-checkbox.beautified { --checkbox-mask-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54z'/%3E%3C/svg%3E");}a-checkbox.beautified::before { border-radius: 8px; }a-checkbox.beautified::after { mask-size: 10px; } /* smaller heart */a-checkbox.beautified:state(checked)::before { background: light-dark(#ec4899, #f472b6); /* pink, both themes */ border-color: transparent;}