Design system

Toggle

Switches a setting or feature on or off.

import { Toggle } from "@/ui";

Usage

Toggle is controlled: pass checked and flip it in onChange. onChange receives the native change event; call sites ignore it and flip their own state.

const [pixel, setPixel] = useState(true);

<Toggle
  label="Include the Petavue tracking pixel"
  checked={pixel}
  onChange={() => setPixel(!pixel)}
/>

The label always sits to the right of the switch, 8px away, and the whole row is clickable. Name the setting in the label, not the action: "Sitemap indexing", not "Turn on sitemap indexing".

With a hint

When the effect of a setting is not obvious, add a 12px --color-text-secondary line underneath that says what the current position means. Indent it 38px so it lines up with the label. Toggle has no hint prop, so this is your own markup.

<Toggle label="Sitemap indexing" checked={sitemap} onChange={() => setSitemap(!sitemap)} />
<p style={{ margin: "4px 0 0 38px", fontSize: 12, color: "var(--color-text-secondary)" }}>
  {sitemap
    ? "Search engines can list this page."
    : "Hidden from search engines. Use for pages that only paid campaigns should reach."}
</p>

In a settings row

To put the switch at the right end of a row with the setting's name on the left, leave out label and pass an aria-label with the same words. Without it, the switch has no name.

<div className="settings-row">
  <span>Sync HubSpot contacts daily</span>
  <Toggle
    aria-label="Sync HubSpot contacts daily"
    checked={sync.hubspot}
    onChange={() => setSync((s) => ({ ...s, hubspot: !s.hubspot }))}
  />
</div>

States

<Toggle label="Off" checked={false} />
<Toggle label="On" checked />
<Toggle label="Off, disabled" checked={false} disabled />
<Toggle label="On, disabled" checked disabled />
<Toggle label="Some on" indeterminate />
StateTrackThumbHover
Off--color-grey-300White, left--color-grey-500 track
On--color-primary-500White, right--color-primary-800 track
Focus (keyboard)--shadow-focus ring
Off, disabled--color-grey-200--color-grey-50none
On, disabled--color-primary-300--color-grey-50none
Intermediate--color-primary-500A 12 by 4px white bar in the middle--color-primary-800 track
Intermediate, disabled--color-primary-300--color-grey-50 barnone

Use indeterminate on a toggle that controls a group, such as "Sync all sources", when only some of the group are on. It matches Figma's Intermediate state, and screen readers hear "mixed".

The track is 30 by 16px with fully rounded ends, and the 14px thumb slides 14px in 0.2s. The label is 14px regular --color-text-primary, and --color-text-disabled when disabled.

There is one size. Some call sites pass size="lg", but Toggle has no size prop: the value ends up as an attribute on the hidden input and changes nothing.

Props

PropTypeDefaultNotes
labelReactNode""Text after the switch. When empty, nothing is rendered and you must pass aria-label.
checkedbooleanfalseControlled.
indeterminatebooleanfalseShows the bar. Takes priority over checked.
onChange(event) => voidReceives the native change event.
disabledbooleanfalse
classNamestringAdded to the wrapping <label>. Use for layout only.

Any other prop (aria-label, aria-describedby, name, ref) is passed to the hidden <input>.

Accessibility

  • It is a native checkbox with role="switch", visually hidden inside a <label>. Space toggles it, and screen readers announce it as a switch that is on or off.
  • The focus ring shows on keyboard focus only (:focus-visible).
  • Several toggles in the product (Data hub status, column and table switches) have no label and no aria-label, so screen readers announce an unnamed switch. Always give one or the other.
  • If you add a hint line, give it an id and pass aria-describedby so it is read with the switch.

Do and don't

  • Do use a Toggle for a setting that is on or off, such as tracking, indexing or a sync, where the state is the point.
  • Do keep the label the same in both positions. Say what changes in a hint line, not by rewriting the label.
  • Don't use a Toggle to pick items from a list or to agree to terms. Use a Checkbox.
  • Don't use a Toggle for a choice between two named options, such as "Monthly" and "Quarterly". Use Radio.

On this page