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

Surface Pair Ratio
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

PropTypeDefaultDescription
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
checkedbooleanfalse[switch] Initial on/off state
labelstringundefined[switch] Visible label rendered next to the track
labelPosition'before' | 'after''after'[switch] Whether the label sits before or after the track
disabledbooleanfalse[switch] Disable interaction
namestringundefined[switch] Identifier emitted in toggle-change detail
valuestringundefined[switch] Value emitted in toggle-change detail
itemsSegmentedItem[][][segmented] Options β€” each `{ value, label, href?, hreflang?, disabled? }`
currentstringundefined[segmented] Value of the active option
groupLabelstring'Options'[segmented] aria-label for the group
reloadbooleanfalse[segmented] Force `window.location.href` on link clicks β€” use when view transitions cannot handle the swap (e.g. locale changes)
classstring''Extra classes on the root element

CSS recipe classes

Class Purpose
sds-toggle-switch Root of a switch β€” flex container with label + track
sds-toggle-track Rounded track holding the thumb
sds-toggle-thumb Sliding white circle inside the track
sds-toggle-label Visible label text
sds-toggle-segmented Root of a segmented group
sds-toggle-seg-btn Individual option β€” link or button