An on or off setting that takes effect as soon as it is flipped, with a label, hint and Changed chip.

Basic

Pass a label, and a hint if the label cannot say it all. A press anywhere on the row, label included, flips the switch.

Example

Changed
A short email when someone replies to your comment.
Follows the page theme and corners
'use client';

import { Switch } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function SwitchBasic(): ReactElement {
  return (
    <Switch description="A short email when someone replies to your comment." defaultSelected>
      Notify me about replies
    </Switch>
  );
}

States

  • On: an ink track with a canvas thumb.
  • Off: the control boundary color with a raised thumb.
  • Disabled: recessed, filled when on and hollow when off, so the two never look alike. The label is taupe, as on a disabled checkbox or radio.
  • Read-only: looks enabled but cannot be changed.

Example

Changed
Changed
Changed
Changed
Changed
Changed
Follows the page theme and corners
'use client';

import { Switch } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function SwitchStates(): ReactElement {
  return (
    <div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
      <Switch defaultSelected>On</Switch>
      <Switch>Off</Switch>
      <Switch isChanged>Changed</Switch>
      <Switch isDisabled defaultSelected>
        Disabled and on
      </Switch>
      <Switch isDisabled>Disabled and off</Switch>
      <Switch isReadOnly defaultSelected>
        Read-only
      </Switch>
    </div>
  );
}

Unsaved changes

Pass isChanged while the setting differs from the saved one. A clay chip shows beside the label, a light signal orange ring goes around the track, and a screen reader reads the chip with the switch. Flip a switch below to see them.

Example

Changed
A short note when something ships.
Changed
Every Tuesday morning.
Changed
Always on for owners.
Follows the page theme and corners
'use client';

import { Button, Switch } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';

const SAVED = { updates: true, digest: false, alerts: true };
type Setting = keyof typeof SAVED;

const ROWS: { key: Setting; label: string; description: string }[] = [
  { key: 'updates', label: 'Product updates', description: 'A short note when something ships.' },
  { key: 'digest', label: 'Weekly digest', description: 'Every Tuesday morning.' },
  { key: 'alerts', label: 'Security alerts', description: 'Always on for owners.' },
];

export default function SwitchSettings(): ReactElement {
  const [saved, setSaved] = useState(SAVED);
  const [draft, setDraft] = useState(SAVED);
  const isDirty = ROWS.some(({ key }) => draft[key] !== saved[key]);
  return (
    <div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
      {ROWS.map(({ key, label, description }) => (
        <Switch
          key={key}
          description={description}
          isSelected={draft[key]}
          onChange={(isSelected) => {
            setDraft({ ...draft, [key]: isSelected });
          }}
          isChanged={draft[key] !== saved[key]}
        >
          {label}
        </Switch>
      ))}
      <div className="row">
        <Button
          isDisabled={!isDirty}
          onPress={() => {
            setSaved(draft);
          }}
        >
          Save changes
        </Button>
        <Button
          variant="tertiary"
          isDisabled={!isDirty}
          onPress={() => {
            setDraft(saved);
          }}
        >
          Discard
        </Button>
      </div>
    </div>
  );
}

Usage

A switch works controlled or uncontrolled, and onChange receives a boolean. With a name, it submits in a plain form. Like a checkbox, it sends its value, or on by default, only while it is on. The types require an accessible name: the switch's text, an aria-label or an aria-labelledby.

import { useState } from 'react';
import { Switch } from '@snapshot-ui/react';

export function DigestSetting({ saved }: { saved: boolean }) {
  const [isOn, setOn] = useState(saved);
  return (
    <Switch
      name="digest"
      value="weekly"
      description="Every Tuesday morning."
      isSelected={isOn}
      onChange={setOn}
      isChanged={isOn !== saved}
    >
      Weekly digest
    </Switch>
  );
}

A switch has no required or invalid state. For a choice that a form confirms later, such as agreeing to terms, use a checkbox.

Props

Every React Aria SwitchField prop is accepted except isRequired, isInvalid and validate. These are the ones Snapshot UI adds or shapes.

changedLabel
string

The text of the Changed chip.

Default: 'Changed'

children
ReactNode

The label, beside the track. Required unless the switch has an aria-label or an aria-labelledby.

defaultSelected
boolean

Whether the element should be selected (uncontrolled).

description
ReactNode

A hint under the label. It becomes the switch's accessible description.

isChanged
boolean

Whether the setting differs from the saved one. Shows the clay Changed chip beside the label, which a screen reader reads with the switch, and a light signal orange ring around the track. Without a visible label, only the ring shows.

isDisabled
boolean

Whether the input is disabled.

isReadOnly
boolean

Whether the input can be selected but not changed by the user.

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.

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).

Accessibility

A native checkbox with the switch role, so a screen reader says the label, then on or off. The label names it, and the hint and the Changed chip describe it. The track and thumb are decoration. The row is a 40px target, though the track is drawn 30px tall.

SpaceTurns the switch on or off.
TabMoves focus to the switch. The 2px ring sits outside the track.

The thumb slides and the track fades over 160ms. Both stop under reduced motion.

A switch with no name does not compile. TypeScript's error either names only aria-labelledby or says the props are not assignable. Both mean the switch has no accessible name.

Design rules

What keeps switches in the system.

Label what the switch controls, in sentence case, such as Weekly digest.

Show the chip and the ring together, and only while the setting is unsaved.

Let the switch act at once, or mark it changed until the page saves.

Never write the label as an action, such as Turn on weekly digest. The thumb already says on or off.

Never use a switch for a choice a form confirms later.

Never use color alone to say a setting is on. The thumb moves too.

esc

↑ ↓ to move↵ to open