Skip to main content
xriptxr

Switch

Info:Controls
71

An on/off toggle that applies immediately, with an accessible name and optional state labels.

toggle on off boolean flip

Live demo

live · @xtyle/astro

Switch

A settings panel

Email notifications

Get a digest when something changes.

Email notifications

Reduced motion

Honor system animation preferences.

Reduced motionOff

Beta channel

Locked while a build is in flight.

Beta channel

Spread & clickable labels

spread stretches each switch across its row, so the tracks line up at the far edge; click a label anywhere to flip its switch. With labelSide="end" the track leads instead.

Autosave drafts Show line numbers Spell check Sync over metered connectionsOff Telemetry Track first, label at the end

States & sizes

Off

off

On

on

With labelsYes

labelled

Small

sm

Disabled

disabled

Shape, orientation, direction & label

Vertical follows a wall switch (down off, up on); reverse flips the on/off direction; labelSide moves the label without touching behavior.

Square

square

Vertical

vertical (up = on)

Reversed

reverse (on at left)

Vertical reversed

vertical + reverse

Label after

labelSide=end

An app-owned cursor

A settings list driven from the app's own input layer never moves DOM focus. cursor marks the row the app's cursor rests on and draws the track's focus ring there, with no ARIA change. The buttons stand in for the app's own input.

Subtitles Screen shake Vibration

Every tone

Toggle all

Toggle any switch: the track lights up while the knob stays a darkened cast of the tone, so it reads in both states. “Toggle all” flips the lot.

Accents

accent accent-2 accent-3 accent-4 neutral

Statuses

success info warn danger

Named hues

red orange yellow green blue purple brown pink cyan gray white black

Switch is a role="switch" toggle for a setting that takes effect immediately, distinct from a checkbox that stages a value until a form submit. It renders a native <button> track with a sliding thumb, so pointer, Space, and Enter all flip it and the state lives in aria-checked.

An optional leading label and optional on-label/off-label state text make the toggle self-describing; clicking the label flips it like a native <label>, and spread stretches the row full width so a column of settings lines its tracks up at the far edge. It is form-associated: give it a name (and optional value) and it contributes to form data only while on. Two sizes: the default md and a compact sm.

When to use

How this component composes with the rest of the set.

Pair with Field or a form to capture the toggle; give it a name so it contributes to submitted data.
Use label for a persistent name, or on-label/off-label for a control whose name is its current state.
For a staged value that only applies on submit, reach for a checkbox instead. A switch applies immediately.

Props

17 props, straight from the manifest.

PropTypeDefaultBindingsDescription
checked boolean false
html svelte astro
The on/off state. Two-way bindable in Svelte; reflected to aria-checked and the element's .checked.
focusable boolean true
html svelte astro
Set false to keep the control out of sequential focus navigation, for an app driving selection from its own keyboard cursor. The host's own tabindex cannot express this, because focus lands on the inner control rather than the host, and setting it there from outside does not survive the next render.
cursor boolean false
html svelte astro
Marks the switch as the one an app-owned keyboard cursor rests on, and draws exactly the track's :focus-visible ring (forced-colors outline included) without moving DOM focus. For a settings list driven from the app's own input layer, usually alongside focusable={false}. Controlled: move it in your own input handler. Changes no ARIA.
disabled boolean false
html svelte astro
Disables interaction; mutes the track, suppresses the overlay, and sets aria-disabled.
size Size
sm md
md
html svelte astro
Control size. Only sm differs from the default.
tone FullTone
accent neutral danger success warn info accent-2 accent-3 accent-4 red orange yellow green blue purple brown pink cyan gray white black
accent
html svelte astro
Color of the on-state track and thumb ink. Any of the semantic roles, accent variants, or named hues.
shape "pill" | "square"
pill square
pill
html svelte astro
Track and thumb corner shape: the default rounded pill, or a squared track for a chunkier control.
orientation "horizontal" | "vertical"
horizontal vertical
horizontal
html svelte astro
Track axis. Horizontal slides the thumb left-to-right (on at the right); vertical stands the track up and follows a wall switch: down is off, up is on.
reverse boolean false
html svelte astro
Flip the on/off direction: horizontal puts on at the left, vertical puts on at the bottom. The label is unaffected.
labelSide html: label-side "start" | "end"
start end
start
html svelte astro
Which side the label sits on relative to the track. end puts the control first and the label after it.
spread boolean false
html svelte astro
Stretch the switch into a full-width row with the label growing to fill it, so the label and the track sit at opposite edges and a column of switches lines its tracks up. Follows labelSide (with end the track leads and the label is end-aligned) and the writing direction; orientation only stands the track up, so a vertical spread switch keeps the same row.
label string —
html svelte astro
Visible leading label, also wired as the accessible name via aria-labelledby.
labelledby string —
html svelte astro
ID of an external element that names the toggle. Takes precedence over label.
onLabel html: on-label string —
html svelte astro
State text shown (and announced as the name, absent label) when on.
offLabel html: off-label string —
html svelte astro
State text shown (and announced as the name, absent label) when off.
name string —
html svelte astro
Form field name; the toggle contributes value to form data only while on.
value string on
html svelte astro
The value submitted with the form when the toggle is on.

Events

What the component emits, and what rides along on event.detail. The name in the first column is the one addEventListener takes.

EventDetailBindingsDescription
change { checked, value }
html svelte astro
The switch was toggled.

Appearance

Variants

square

.xtyle-switch--square

Squared track and thumb corners.

vertical

.xtyle-switch--vertical

Track stood on its end; down is off, up is on.

reverse

.xtyle-switch--reverse

On/off direction flipped (on at the left, or the bottom when vertical).

label-end

.xtyle-switch--label-end

Label after the control instead of before it.

spread

.xtyle-switch--spread

A full-width row: label and track at opposite edges.

Sizes

sm

.xtyle-switch--sm

Compact.

md

default
.xtyle-switch

Default.

States

on

.xtyle-switch__track[aria-checked="true"]

Toggle is on: the track fills with the chosen tone (accent by default) and the thumb slides to the end.

hover

.xtyle-switch__track:hover::after

Pointer over the track: overlay paints the hover tint.

active

.xtyle-switch__track:active::after

Track pressed: overlay paints the press tint.

focus-visible

.xtyle-switch__track:focus-visible

Keyboard focus: a token-colored ring, plus a transparent outline that becomes real in forced-colors mode.

cursor

.xtyle-switch__track[data-cursor]

App-owned keyboard cursor: cursor puts data-cursor on the track, which shares the focus-visible rule, so it draws the same ring and the same forced-colors outline while DOM focus stays where it is.

disabled

.xtyle-switch__track:disabled, .xtyle-switch__track[aria-disabled="true"]

Non-interactive: muted track and thumb, overlay suppressed.

Anatomy

The named parts that make up the component. Created from script it renders a shadow root, where a ::part() handle is reachable from your own stylesheet and the class beside it is internal. Upgraded over server-rendered markup it renders light DOM, where the class is the handle.

switch

Success:::part(switch) .xtyle-switch

The row wrapper carrying the size, variant, and disabled classes and laying out label, track, and state text; inline by default, a full-width row under spread.

--font-sans --text-body --leading-tight --fg-0 --space-2

track

Success:::part(track) .xtyle-switch__track

The interactive role="switch" button: the pill the thumb slides within.

--space-7 --space-5 --space-2 --border-thin --line-strong --radius-full --neutral-bg --duration-base --ease-emphasized --duration-fast --ease-standard

thumb

Success:::part(thumb) .xtyle-switch__thumb

The circular knob: a darkened cast of the track's tone, kept visible on and off.

--space-1 --radius-full --elevation-1

label

Success:::part(label) .xtyle-switch__label

The visible leading label that names the toggle and is referenced as its accessible name. It is a pointer target: a click flips the switch once, as a native <label> would, and does nothing while disabled. It takes no focus; the track stays the one focusable control. Under spread it grows to fill the row.

state

Success:::part(state) .xtyle-switch__state

Optional trailing on/off state text; becomes the accessible name when no other label is given.

--fg-2 --text-sm

overlay

internal .xtyle-switch__track::after

The pseudo-element behind the track that paints hover and active state tints.

--state-hover --state-press

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 71/71 consumed tokens produced default register: 311 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors }) → coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --accent-2 --accent-2-fg --accent-3 --accent-3-fg --accent-4 --accent-4-fg --accent-fg --black --black-fg --blue --blue-fg --border-normal --border-thick --border-thin --brown --brown-fg --cyan --cyan-fg --danger --danger-fg --duration-base --duration-fast --ease-emphasized --ease-standard --elevation-1 --fg-0 --fg-2 --fg-disabled --font-sans --gray --gray-fg --green --green-fg --info --info-fg --leading-tight --line-strong --neutral --neutral-bg --neutral-fg --orange --orange-fg --pink --pink-fg --purple --purple-fg --radius-full --radius-sm --red --red-fg --ring --space-1 --space-2 --space-4 --space-5 --space-6 --space-7 --state-disabled --state-hover --state-press --success --success-fg --text-body --text-sm --warn --warn-fg --white --white-fg --yellow --yellow-fg

Accessibility

The track's edge is drawn in --line-strong, so the off state still shows where the control is (3:1 against the page).
The host's aria-label / aria-labelledby names the inner role="switch" track, ahead of the component's own naming; aria-labelledby ids are read from the host's own tree.
Under forced-colors: active the thumb draws in CanvasText, and an on track fills with Highlight behind a HighlightText thumb (GrayText when disabled); the thumb is a background the mode would otherwise flatten to Canvas, leaving on and off identical.
Renders a native <button> with role="switch", so click, Space, and Enter all toggle it; Space's default scroll is prevented.
The visible label is a click target like a native <label>: one click flips the switch once and a disabled switch ignores it. The label is not focusable, so keyboard users still reach and operate the track alone.
State is carried in aria-checked and mirrored on the element's reflected .checked, never by color alone.
Requires an accessible name: labelledby wins, then label, then the on/off state text; the binding warns at runtime when none is present.
The thumb and state-only labels are wired so the toggle is always announced with a name, even when only on-label/off-label are given.
Focus is shown with a token ring and a transparent outline that the forced-colors base rule promotes to a real system outline.
cursor is visual only and sets no ARIA: an app-owned cursor is not focus, so it is not announced as focus, and the switch's state stays in aria-checked alone. An app that needs the cursor position announced owns that channel itself, typically by moving real focus or with a live region.
disabled blocks interaction and sets aria-disabled; the track is muted and the overlay suppressed.

Code

Labels, tones, shapes, and form association

A named toggle, on/off state text, the compact size, a toned switch, the square / vertical / reversed variants, a form-bound switch, and a disabled one.

<xtyle-switch label="Wi-Fi" checked></xtyle-switch>

<xtyle-switch label="Notifications" on-label="On" off-label="Off"></xtyle-switch>

<xtyle-switch size="sm" label="Compact mode"></xtyle-switch>

<xtyle-switch label="Alerts" tone="danger" checked></xtyle-switch>

<xtyle-switch label="Square" shape="square" checked></xtyle-switch>

<xtyle-switch label="Vertical" orientation="vertical" checked></xtyle-switch>

<xtyle-switch label="Reversed" reverse checked></xtyle-switch>

<xtyle-switch label="Label after" label-side="end" checked></xtyle-switch>

<xtyle-switch label="Sync" name="sync" value="enabled" checked></xtyle-switch>

<xtyle-switch label="Locked" disabled checked></xtyle-switch>

A spread settings column

spread stretches each switch across its row, so the labels sit at the start, the tracks line up at the end, and a click anywhere on a label flips its switch.

<xtyle-stack gap="3">
	<xtyle-switch spread label="Email notifications" checked></xtyle-switch>
	<xtyle-switch spread label="Reduced motion"></xtyle-switch>
	<xtyle-switch spread label="Beta channel" disabled checked></xtyle-switch>
</xtyle-stack>

An app-owned keyboard cursor

A settings list driven from the app's own input layer rather than DOM focus: focusable={false} keeps the browser's tab stop out of it, and cursor on the current row draws the focus ring on its track. input stands in for the app's own action bindings.

<xtyle-stack gap="3" id="settings">
	<xtyle-switch spread label="Subtitles" focusable="false" cursor checked></xtyle-switch>
	<xtyle-switch spread label="Screen shake" focusable="false"></xtyle-switch>
	<xtyle-switch spread label="Vibration" focusable="false" checked></xtyle-switch>
</xtyle-stack>

<script>
	const rows = [...document.querySelectorAll("#settings xtyle-switch")];
	let at = 0;
	function moveCursor(step) {
		rows[at].cursor = false;
		at = (at + step + rows.length) % rows.length;
		rows[at].cursor = true;
	}
	input.on("menu-up", () => moveCursor(-1));
	input.on("menu-down", () => moveCursor(1));
	input.on("confirm", () => (rows[at].checked = !rows[at].checked));
</script>