Start typing to search components and docs…
↡ select ↑ ↓ navigate ESC close

Toggle

Universal toggle component with two semantic shapes:

  • switch β€” binary on/off (one boolean setting). Uses role="switch", aria-checked.
  • segmented β€” mutually exclusive options (2+ values). Uses role="group" with aria-current="page" for navigation links, or role="radio" + aria-checked for state-only buttons.

Powers language switchers, theme pickers, filters, view-mode toggles β€” anything where the UI shape is β€œpick exactly one”.

Toggle.astro is Astro-only (uses <script>). Import via subpath, not the barrel.

Switch β€” boolean filter

The gbx-toolbar β€œEU market only” filter pattern: a single setting, on/off, with a visible label next to the track.

---
import Toggle from 'spoko-design-system/components/Toggle.astro';
---
<Toggle variant="switch" label="EU market only" checked name="eu-only" />

Off state

<Toggle variant="switch" label="Show archived" name="show-archived" />

Label position

<Toggle variant="switch" label="Notifications" checked labelPosition="before" />
<Toggle variant="switch" label="Notifications" checked labelPosition="after" />

Sizes

<Toggle variant="switch" size="sm" label="Small" checked />
<Toggle variant="switch" size="md" label="Medium" checked />
<Toggle variant="switch" size="lg" label="Large" checked />

Disabled

<Toggle variant="switch" label="Feature locked" disabled />

Segmented β€” language switcher

Navigation-style switcher where each option is an anchor with a pre-resolved URL. Active option marked with aria-current="page". Pass reload for stacks where view transitions can’t safely swap the locale (forces window.location.href).

<Toggle
variant="segmented"
groupLabel="Language"
current="en"
reload
items={[
  { value: 'en', label: 'EN', href: '/gearboxes/', hreflang: 'en' },
  { value: 'pl', label: 'PL', href: '/pl/skrzynie-biegow/', hreflang: 'pl' },
  { value: 'de', label: 'DE', href: '/de/getriebe/', hreflang: 'de' },
]}
/>

Segmented β€” state-only (theme picker)

When items have no href, the component renders <button role="radio"> with arrow-key navigation between options and emits a toggle-change CustomEvent.

<Toggle
variant="segmented"
groupLabel="Theme"
current="auto"
items={[
  { value: 'light', label: 'Light' },
  { value: 'dark', label: 'Dark' },
  { value: 'auto', label: 'Auto' },
]}
/>

Segmented β€” view mode

<Toggle
variant="segmented"
groupLabel="View"
current="grid"
items={[
  { value: 'list', label: 'List' },
  { value: 'grid', label: 'Grid' },
  { value: 'compact', label: 'Compact' },
]}
/>

Segmented β€” sizes

<Toggle variant="segmented" size="sm" groupLabel="Range" current="d"
items={[
  { value: 'd', label: 'Day' },
  { value: 'w', label: 'Week' },
  { value: 'm', label: 'Month' },
]} />
<Toggle variant="segmented" size="md" groupLabel="Range" current="d"
items={[
  { value: 'd', label: 'Day' },
  { value: 'w', label: 'Week' },
  { value: 'm', label: 'Month' },
]} />
<Toggle variant="segmented" size="lg" groupLabel="Range" current="d"
items={[
  { value: 'd', label: 'Day' },
  { value: 'w', label: 'Week' },
  { value: 'm', label: 'Month' },
]} />

Segmented β€” with disabled option

<Toggle
variant="segmented"
groupLabel="Plan"
current="pro"
items={[
  { value: 'free', label: 'Free' },
  { value: 'pro', label: 'Pro' },
  { value: 'enterprise', label: 'Enterprise', disabled: true },
]}
/>

Styled looks

Three opt-in recipes ported from real-world toggles. Apply via look prop.

look="pill" β€” solid-fill segmented (catalog-style)

White pill container with a thin tinted border. Active option is filled solid with the darkest blue and white text β€” strong visual emphasis, no shadow trick.

<Toggle
variant="segmented"
look="pill"
groupLabel="Language"
current="en"
reload
items={[
  { value: 'en', label: 'EN', href: '/', hreflang: 'en' },
  { value: 'pl', label: 'PL', href: '/pl/', hreflang: 'pl' },
  { value: 'de', label: 'DE', href: '/de/', hreflang: 'de' },
]}
/>

look="spoko" β€” transparent pill with dark active fill

Transparent container with a softer outline. Active option uses a near-black navy on light backgrounds and inverts to near-white in dark mode.

<Toggle
variant="segmented"
look="spoko"
groupLabel="Language"
current="en"
items={[
  { value: 'en', label: 'EN', href: '/', hreflang: 'en' },
  { value: 'pl', label: 'PL', href: '/pl/', hreflang: 'pl' },
]}
/>

look="outline" β€” switch wrapped in an outlined frame

The catalog toggler pattern β€” track + thumb + label sit inside a rounded border that tints toward the accent on hover and turns into the accent colour when on. Useful when the switch needs to read as a discrete chip rather than a bare slider.

<Toggle
variant="switch"
look="outline"
checked
label="EU market only"
/>
<Toggle
variant="switch"
look="outline"
label="Show archived"
/>

Listening to changes

Both variants dispatch a bubbling toggle-change CustomEvent.

document.addEventListener('toggle-change', (e) => {
  const detail = (e as CustomEvent).detail;
  // switch:   { name?, value?, checked: boolean }
  // segmented: { value: string }
  console.log(detail);
});

Accessibility

All colour pairs in the default style meet WCAG AA contrast (β‰₯4.5:1 for text, β‰₯3:1 for UI components):

SurfacePairRatio
Active segmented option (bg-white + text-blue-medium #02307d)text vs backgroundβ‰ˆ 15:1
Inactive segmented option (bg-slate-100 + text-slate-dark #334155)text vs backgroundβ‰ˆ 9:1
Switch ON track (bg-blue-medium + white thumb)thumb vs trackβ‰ˆ 15:1
Switch OFF track (bg-slate-light #64748B + white thumb)thumb vs trackβ‰ˆ 4.7:1
Disabled (opacity 0.5)text vs backgroundβ‰₯ 4.5:1

ARIA contract:

  • Switch: role="switch", aria-checked toggled on click/space/enter, disabled propagated, focus ring via :focus-visible.
  • Segmented (links): role="group" + aria-label, active item gets aria-current="page", hreflang propagated when supplied.
  • Segmented (radios): role="group" + aria-label, each option role="radio" + aria-checked, only the active option is tab-stop (others tabindex="-1"), ←/β†’ moves focus and updates selection.

Props

Prop Type Default Description
variant 'switch' | 'segmented' 'switch' Toggle shape β€” boolean switch or mutually-exclusive options
size 'sm' | 'md' | 'lg' 'md' Visual size of the control (only honored by `look="default"`)
look 'default' | 'pill' | 'spoko' | 'outline' 'default' Visual style. `pill` and `spoko` apply to segmented; `outline` applies to switch
checked boolean false [switch] Initial on/off state
label string undefined [switch] Visible label rendered next to the track
labelPosition 'before' | 'after' 'after' [switch] Whether the label sits before or after the track
disabled boolean false [switch] Disable interaction
name string undefined [switch] Identifier emitted in toggle-change detail
value string undefined [switch] Value emitted in toggle-change detail
items SegmentedItem[] [] [segmented] Options β€” each `{ value, label, href?, hreflang?, disabled? }`
current string undefined [segmented] Value of the active option
groupLabel string 'Options' [segmented] aria-label for the group
reload boolean false [segmented] Force `window.location.href` on link clicks β€” use when view transitions cannot handle the swap (e.g. locale changes)
class string '' Extra classes on the root element

CSS recipe classes

ClassPurpose
sds-toggle-switchRoot of a switch β€” flex container with label + track
sds-toggle-trackRounded track holding the thumb
sds-toggle-thumbSliding white circle inside the track
sds-toggle-labelVisible label text
sds-toggle-segmentedRoot of a segmented group
sds-toggle-seg-btnIndividual option β€” link or button