Checkbox is a compound, accessible toggle control. The root owns visual state
(size, plus inherited invalid / disabled / required / optional /
readOnly), and Checkbox.Indicator is the bordered box that hosts the icon.
Wrap Checkbox in a Field to attach a form-level label, description, or error
message — the field-level state cascades into the checkbox automatically.
Usage
import { Checkbox, Field } from '@takeoff-ui/react-spar';
<Field>
<Field.Label />
<Checkbox>
<Checkbox.Indicator />
</Checkbox>
<Field.Description />
<Field.ErrorMessage />
</Field>
Playground
function PlaygroundDemo() {
return (
<Field>
<Field.Label>Notifications</Field.Label>
<Checkbox defaultChecked>
<Checkbox.Indicator />
</Checkbox>
<Field.Description>We will only email you when a saved route drops in price.</Field.Description>
</Field>
);
}
render(<PlaygroundDemo />);
Using with Field
Field is the generic ARIA wrapper. Setting invalid, disabled, required,
optional, or readOnly on Field cascades into the nested Checkbox, and
Field.Label, Field.Description, and Field.ErrorMessage are wired to the
field control through shared IDs.
function FieldWithCheckboxDemo() {
return (
<div className="grid gap-3">
<Field required>
<Field.Label>Email me booking updates</Field.Label>
<Checkbox defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field>
<Field.Label>Subscribe to fare alerts</Field.Label>
<Checkbox>
<Checkbox.Indicator />
</Checkbox>
<Field.Description>We will only email you when a saved route drops in price.</Field.Description>
</Field>
<Field invalid>
<Field.Label>I accept the booking terms</Field.Label>
<Checkbox>
<Checkbox.Indicator />
</Checkbox>
<Field.ErrorMessage>You must accept the terms to continue.</Field.ErrorMessage>
</Field>
<Field disabled>
<Field.Label>Disabled option</Field.Label>
<Checkbox>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field readOnly>
<Field.Label>Read-only selection</Field.Label>
<Checkbox defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
</div>
);
}
render(<FieldWithCheckboxDemo />);
Controlled
function ControlledCheckboxDemo() {
const [accepted, setAccepted] = useState(false);
return (
<div className="grid gap-3">
<Badge className="w-fit" variant="neutral" appearance="outlined">
Terms {accepted ? 'accepted' : 'not accepted'}
</Badge>
<Field>
<Field.Label>I accept the booking terms</Field.Label>
<Checkbox checked={accepted} onChange={setAccepted}>
<Checkbox.Indicator />
</Checkbox>
</Field>
</div>
);
}
render(<ControlledCheckboxDemo />);
Indeterminate
indeterminate overrides checked and defaultChecked while set, and emits
aria-checked="mixed". The first user toggle transitions out of the mixed state
— onChange is always called with a plain boolean. In uncontrolled mode, set
indeterminate only on the initial render and clear it from your own state
after the first change.
function IndeterminateCheckboxDemo() {
const [items, setItems] = useState({
seat: true,
meal: false,
baggage: false,
});
const values = Object.values(items);
const allChecked = values.every(Boolean);
const noneChecked = values.every((v) => !v);
const indeterminate = !allChecked && !noneChecked;
const toggleAll = (next) => {
setItems({ seat: next, meal: next, baggage: next });
};
return (
<div className="grid gap-3">
<Field>
<Field.Label>All extras</Field.Label>
<Checkbox checked={allChecked} indeterminate={indeterminate} onChange={toggleAll}>
<Checkbox.Indicator />
</Checkbox>
</Field>
<div className="grid gap-3" style={{ paddingLeft: 24 }}>
<Field>
<Field.Label>Extra legroom seat</Field.Label>
<Checkbox checked={items.seat} onChange={(v) => setItems({ ...items, seat: v })}>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field>
<Field.Label>Special meal</Field.Label>
<Checkbox checked={items.meal} onChange={(v) => setItems({ ...items, meal: v })}>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field>
<Field.Label>Extra baggage</Field.Label>
<Checkbox checked={items.baggage} onChange={(v) => setItems({ ...items, baggage: v })}>
<Checkbox.Indicator />
</Checkbox>
</Field>
</div>
</div>
);
}
render(<IndeterminateCheckboxDemo />);
Sizes
function SizeDemo() {
return (
<div className="flex flex-wrap justify-center gap-6">
<Field>
<Field.Label>Small</Field.Label>
<Checkbox size="small" defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field>
<Field.Label>Base</Field.Label>
<Checkbox size="base" defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
</div>
);
}
render(<SizeDemo />);
Custom Icon
Checkbox.Indicator accepts function children that receive the current
checked and indeterminate state, so you can render different glyphs per
state without mounting two icons. Passing a ReactNode instead replaces the
default icon slot entirely.
function CustomIconDemo() {
return (
<div className="grid gap-3">
<Field>
<Field.Label>Favorite this route</Field.Label>
<Checkbox defaultChecked>
<Checkbox.Indicator>
{({ checked }) => (checked ? <StarIconOutlinedRounded /> : null)}
</Checkbox.Indicator>
</Checkbox>
</Field>
</div>
);
}
render(<CustomIconDemo />);
Disabled, Read-Only, and Invalid
function StateDemo() {
return (
<div className="grid gap-3">
<Field disabled>
<Field.Label>Disabled option</Field.Label>
<Checkbox>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field disabled>
<Field.Label>Disabled and selected</Field.Label>
<Checkbox defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field readOnly>
<Field.Label>Read-only selection</Field.Label>
<Checkbox defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field invalid required>
<Field.Label>I accept the booking terms</Field.Label>
<Checkbox>
<Checkbox.Indicator />
</Checkbox>
<Field.ErrorMessage>You must accept the terms to continue.</Field.ErrorMessage>
</Field>
</div>
);
}
render(<StateDemo />);
When name is set, Spar renders a synchronized hidden checkbox input for native
form submission. Multiple checkboxes sharing the same name produce an array
via FormData.getAll.
function CheckboxFormDemo() {
const [saved, setSaved] = useState('Not submitted');
return (
<form
className="grid gap-3"
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const picked = data.getAll('extras');
setSaved(picked.length ? picked.join(', ') : 'No extras');
}}
>
<Field>
<Field.Label>Extra legroom seat</Field.Label>
<Checkbox name="extras" value="seat" defaultChecked>
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field>
<Field.Label>Special meal</Field.Label>
<Checkbox name="extras" value="meal">
<Checkbox.Indicator />
</Checkbox>
</Field>
<Field>
<Field.Label>Extra baggage</Field.Label>
<Checkbox name="extras" value="baggage">
<Checkbox.Indicator />
</Checkbox>
</Field>
<div className="flex flex-wrap items-center gap-3">
<Button type="submit">Save</Button>
<Badge variant="neutral" appearance="outlined">
{saved}
</Badge>
</div>
</form>
);
}
render(<CheckboxFormDemo />);
Accessibility
Field.Label, Field.Description, and Field.ErrorMessage are wired to the
control via stable IDs derived from the Field root (aria-labelledby,
aria-describedby, aria-invalid).
Checkbox renders a focusable control with role="checkbox" and reflects its
value through aria-checked (including "mixed" for the indeterminate
state).
- The asterisk inside
Field.Label is decorative — required is also surfaced
to assistive tech via the input's native required / aria-required.
- Disabled checkboxes are removed from the tab order. Read-only checkboxes
remain focusable and do not change value.
| Key | Behavior |
|---|
| Space | Toggle the focused checkbox. |
| Tab | Move focus to the next tabbable UI. |
API Reference
Checkbox
See
Spar Checkbox docs
for primitive behavior.
Props
| Name | Type | Default | Description |
|---|
| children | React.ReactNode | ((state: CheckboxRenderProps) => React.ReactNode) | - | Compound children for checkbox anatomy, or a render function exposing Spar tri-state. |
| indeterminate | boolean | false | Indeterminate (mixed) visual + ARIA state. Overrides checked / defaultChecked and emits aria-checked="mixed". |
| invalid | boolean | false | Marks the checkbox as visually invalid. |
| size | CheckboxSize | 'base' | Size scale. |
| classNames | Partial<Record<CheckboxSlot, string>> | - | Per-slot class name overrides. |
| slotProps | Partial<Record<CheckboxSlot, React.HTMLAttributes<HTMLElement>>> | - | Per-slot HTML attribute overrides. |
| className | string | - | Appends custom classes to the root slot of this part. |
Data attributes
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for the root slot. |
| data-size | Always | Reflects the resolved size prop so theme recipes can scope size variants. |
| data-invalid | invalid is true. | Marks invalid visual state for theme recipes. |
| data-checked | When checked. | Spar checked-state hook. |
| data-indeterminate | When indeterminate. | Spar indeterminate-state hook. |
| data-disabled | disabled is true. | Spar disabled-state hook. |
| data-readonly | readOnly is true. | Spar read-only-state hook. |
| data-required | required is true. | Spar required-state hook. |
Checkbox.Indicator
Data attributes
| Attribute | Applied when | Purpose |
|---|
| data-slot="indicator" | Always | Stable selector for the indicator slot. |
| data-slot="icon" | Always | Stable selector for the icon slot. |
Type Definitions
| Name | Definition |
|---|
| CheckboxRenderProps | { checked: CheckedState; setChecked: (checked: CheckedState) => void; disabled: boolean; readOnly: boolean; required: boolean; invalid: boolean; isFocused: boolean; isHovered: boolean; isPressed: boolean } |
| CheckboxSize | 'small' | 'base' |
| CheckboxSlot | 'root' | 'indicator' | 'icon' |
| CheckboxIndicatorRenderProps | { checked: boolean; indeterminate: boolean } |