A single checkbox or a labeled group, where the whole row is the target.
Basic
The label sits beside the box, and a press anywhere on the row toggles it. A hint goes under the label's text.
Example
Example
'use client';
import { Checkbox } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function CheckboxBasic(): ReactElement {
return (
<div style={{ display: 'grid', width: '100%', maxWidth: '24rem' }}>
<Checkbox name="remember" defaultSelected>
Remember me on this device
</Checkbox>
<Checkbox name="descriptions" description="Off hides the copy under each service card.">
Show service descriptions
</Checkbox>
</div>
);
}States
- Checked: an ink fill with a canvas mark, drawn with a mask so it reads in both themes.
- Indeterminate: a canvas bar.
- Invalid: only the edge turns to the error color, so an invalid checked box still reads as checked.
- Disabled: recessed, with a taupe label.
- Read-only: looks enabled but cannot be pressed.
- Changed: a clay chip beside the label and nothing else, read with the checkbox.
Invalid outranks changed, so the chip gives way to the error line. A checkbox with no visible label shows no chip. Inside a group, a checkbox ignores isChanged; the group's own chip covers it.
Example
Example
'use client';
import { Checkbox } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function CheckboxStates(): ReactElement {
return (
<div style={{ display: 'grid', width: '100%', maxWidth: '24rem' }}>
<Checkbox defaultSelected>Publish changes right away</Checkbox>
<Checkbox isIndeterminate>Select every page</Checkbox>
<Checkbox isInvalid errorMessage="Accept the terms to continue.">
I accept the terms
</Checkbox>
<Checkbox isDisabled>Publish on a schedule</Checkbox>
<Checkbox isDisabled defaultSelected>
Back up every night
</Checkbox>
<Checkbox isReadOnly defaultSelected>
Managed by your workspace admin
</Checkbox>
<Checkbox isChanged>Email me a weekly summary</Checkbox>
</div>
);
}Group
A group names several checkboxes and holds their values as one array. It is vertical by default. orientation="horizontal" lays the rows side by side and wraps them.
The first group is controlled and starts at its saved selection. Toggle a box and isChanged shows the clay chip beside the label, until the selection matches the saved one again. The chip marks the field, so the boxes keep their edges.
Example
Example
'use client';
import { Checkbox, CheckboxGroup } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';
const SAVED = ['email'];
export default function CheckboxGroupExample(): ReactElement {
const [channels, setChannels] = useState(SAVED);
const isChanged = channels.length !== SAVED.length || channels.some((channel) => !SAVED.includes(channel));
return (
<div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
<CheckboxGroup
label="Notify me by"
description="Pick as many as you like."
name="channels"
value={channels}
onChange={setChannels}
isChanged={isChanged}
>
<Checkbox value="email">Email</Checkbox>
<Checkbox value="sms">Text message</Checkbox>
<Checkbox value="push">Push notification</Checkbox>
</CheckboxGroup>
<CheckboxGroup label="Show in the summary" orientation="horizontal" defaultValue={['pages']}>
<Checkbox value="pages">Pages</Checkbox>
<Checkbox value="comments">Comments</Checkbox>
<Checkbox value="traffic">Traffic</Checkbox>
</CheckboxGroup>
</div>
);
}A form reset returns a group to its defaultValue. A reset that only checks boxes, or unchecks a single box, works. Two cases fail because of React Aria:
- A reset that unchecks more than one box leaves all but the last of them checked.
- A reset that checks a box and unchecks one further down the group loses the checked box.
Select all
A parent checkbox is indeterminate while only some of its children are checked. Derive isIndeterminate and isSelected from the children's state. Pressing an indeterminate checkbox reports true, which here checks every child.
Example
Example
'use client';
import { Checkbox, CheckboxGroup } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';
const CHANNELS = ['email', 'sms', 'push'];
export default function CheckboxSelectAll(): ReactElement {
const [selected, setSelected] = useState(['email']);
return (
<div style={{ width: '100%', maxWidth: '24rem' }}>
<Checkbox
isSelected={selected.length === CHANNELS.length}
isIndeterminate={selected.length > 0 && selected.length < CHANNELS.length}
onChange={(isSelected) => {
setSelected(isSelected ? CHANNELS : []);
}}
>
Select all channels
</Checkbox>
<CheckboxGroup
aria-label="Channels"
value={selected}
onChange={setSelected}
style={{ marginInlineStart: '2rem' }}
>
<Checkbox value="email">Email</Checkbox>
<Checkbox value="sms">Text message</Checkbox>
<Checkbox value="push">Push notification</Checkbox>
</CheckboxGroup>
</div>
);
}Validation
Set isRequired on a checkbox, or on a group where one checked box is enough, and the browser checks it on submit. The error line shows the message, and every box in an invalid group gets the error edge. A validate function works too. A group shows only its own error line, so an errorMessage on a checkbox inside it is not shown.
Example
Example
'use client';
import { Button, Checkbox, CheckboxGroup, Text } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';
export default function CheckboxValidation(): ReactElement {
const [saved, setSaved] = useState('');
return (
<form
className="stack"
style={{ width: '100%', maxWidth: '24rem' }}
onSubmit={(event) => {
event.preventDefault();
const channels = new FormData(event.currentTarget)
.getAll('channels')
.filter((entry) => typeof entry === 'string');
setSaved(`Saved: ${channels.join(', ')}.`);
}}
>
<CheckboxGroup
label="Notify me by"
name="channels"
isRequired
errorMessage="Pick at least one way to be notified."
>
<Checkbox value="email">Email</Checkbox>
<Checkbox value="sms">Text message</Checkbox>
<Checkbox value="push">Push notification</Checkbox>
</CheckboxGroup>
<Checkbox name="terms" isRequired errorMessage="Accept the terms to continue.">
I accept the terms
</Checkbox>
<div className="row">
<Button type="submit">Save preferences</Button>
</div>
<Text tone="muted" role="status">
{saved}
</Text>
</form>
);
}Usage
Every value works controlled and uncontrolled. A checked checkbox with a name submits its value, or "on". An unchecked one is left out of the form. A group submits one entry per checked box under the group's name.
import { useState } from 'react';
import { Checkbox, CheckboxGroup } from '@snapshot-ui/react';
export function Preferences() {
const [channels, setChannels] = useState(['email']);
return (
<form>
<CheckboxGroup label="Notify me by" name="channels" value={channels} onChange={setChannels}>
<Checkbox value="email">Email</Checkbox>
<Checkbox value="sms">Text message</Checkbox>
</CheckboxGroup>
<Checkbox name="remember" defaultSelected>
Remember me
</Checkbox>
</form>
);
}The types require a name: text, aria-label or aria-labelledby for a checkbox, and label, aria-label or aria-labelledby for a group.
Props
Every React Aria checkbox prop is accepted. These are the ones Snapshot UI adds or shapes.
Checkbox
changedLabel string | The text of the Changed chip. Default: 'Changed' |
|---|---|
children ReactNode | The label, shown beside the box. A press anywhere on the row toggles the box.
Required unless the checkbox has an |
defaultSelected boolean | Whether the element should be selected (uncontrolled). |
description ReactNode | A hint under the label. It becomes the checkbox's accessible description. |
errorMessage ((validation: ValidationResult) => ReactNode) | ReactNode | The error line, shown only while invalid. A function receives the validation
result. Without it, the checkbox shows its own validation messages. Not shown
inside a |
isChanged boolean | Whether the choice differs from the saved one. Shows the clay Changed chip
beside the label, read with the checkbox; the box keeps its edge. An invalid
checkbox shows its error instead, and one without a visible label shows no
chip. Ignored inside a |
isDisabled boolean | Whether the input is disabled. |
isIndeterminate boolean | Indeterminism is presentational only. The indeterminate visual representation remains regardless of user interaction. |
isInvalid boolean | Whether the input value is invalid. |
isReadOnly boolean | Whether the input can be selected but not changed by the user. |
isRequired boolean | Whether user input is required on the input before form submission. |
isSelected boolean | Whether the element should be selected (controlled). |
name string | The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). |
onChange ((isSelected: boolean) => void) | Handler that is called when the element's selection state changes. |
validate ((value: boolean) => ValidationError | true | null | undefined) | A function that returns an error message if a given value is invalid.
Validation errors are displayed to the user when the form is submitted
if |
value string | The value of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefvalue). |
CheckboxGroup
changedLabel string | The text of the Changed chip. Default: 'Changed' |
|---|---|
defaultValue string[] | The default value (uncontrolled). |
description ReactNode | A hint under the checkboxes. It becomes the group's accessible description. |
errorMessage ((validation: ValidationResult) => ReactNode) | ReactNode | The error line, shown only while the group is invalid. A function receives the validation result. Without it the group shows its own validation messages. |
isChanged boolean | Whether the selection differs from the saved one. Shows the clay Changed chip in the label row, read with the group; the boxes keep their edges. An invalid group shows its error instead. |
isDisabled boolean | Whether the input is disabled. |
isInvalid boolean | Whether the input value is invalid. |
isReadOnly boolean | Whether the input can be selected but not changed by the user. |
isRequired boolean | Whether user input is required on the input before form submission. |
label ReactNode | The group's name, shown above the checkboxes. Required unless the group has an |
name string | The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). |
onChange ((value: string[]) => void) | Handler that is called when the value changes. |
orientation CheckboxGroupOrientation | How the checkboxes are laid out. Arrow keys do not move between checkboxes in either layout: Tab does. Default: 'vertical' |
validate ((value: string[]) => ValidationError | true | null | undefined) | A function that returns an error message if a given value is invalid.
Validation errors are displayed to the user when the form is submitted
if |
value string[] | The current value (controlled). |
Accessibility
A native checkbox input inside its label, wired by React Aria. The indeterminate state is exposed as mixed. A group is a group named by its label, and its hint and error line describe every checkbox in it.
- A group's Changed chip is read with the group.
- A standalone checkbox's chip is read as part of its description, after the hint, while changed and valid. It sits beside the row, so it is not part of the name and pressing it does not toggle the box.
- An invalid group or checkbox reads its error line alone.
| Tab | Moves to the next checkbox. Each one is its own stop, in a group too. |
|---|---|
| Space | Toggles the focused checkbox. |
| Arrowkeys | Do nothing. Only a radio group moves between its choices with them. |
The row is at least 40px tall, with a 20px box. A checkbox with no text, named by aria-label, keeps a 40px square target that overhangs the box by 10px on each side, so leave 10px beside a scroll edge. In Windows high contrast the box takes the system colors, so the mark never disappears.
A checkbox or group with no name does not compile. TypeScript's error either names only aria-labelledby or says the props are not assignable. Both mean it has no accessible name.
Design rules
What keeps checkboxes in the system.
Name each checkbox in its own label, in sentence case, and use the hint for what the label cannot say.
Use a group when several choices answer one question, and say what is wrong in the error line.
Keep the whole row as the target, with the label beside the box.
Never use a checkbox for a setting that takes effect the moment it is pressed. That is a switch.
Never use a group when only one choice is allowed. That is a radio group.
Never leave the chip beside an error. Invalid outranks changed.