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

Changed
Changed
Off hides the copy under each service card.
Follows the page theme and corners
'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

Changed
Changed
Accept the terms to continue.
Changed
Changed
Changed
Changed
Follows the page theme and corners
'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

Notify me byChanged
Pick as many as you like.
Show in the summaryChanged
Follows the page theme and corners
'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

Changed
Follows the page theme and corners
'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

Notify me byChanged
Changed

Follows the page theme and corners
'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 aria-label or aria-labelledby.

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 CheckboxGroup, which has one error line of its own.

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 CheckboxGroup: set isChanged on the group.

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 validationBehavior="native". For realtime validation, use the isInvalid prop instead.

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 aria-label or an aria-labelledby.

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 validationBehavior="native". For realtime validation, use the isInvalid prop instead.

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.
TabMoves to the next checkbox. Each one is its own stop, in a group too.
SpaceToggles the focused checkbox.
ArrowkeysDo 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.

esc

↑ ↓ to move↵ to open