Skip to content

Toggle

A <label> styled like a button, wrapping a native <input type="checkbox"> (binary toggle) or <input type="radio"> (exclusive group). The label paints the chrome; the input keeps form semantics + keyboard handling + native single-open via the name attribute. Pressed state is driven by :has(input:checked) — no JavaScript.

Install

import "elements-kit/ui/styles.css";
// neutral palette:
import "elements-kit/ui/styles/palette/gray.css";
import "elements-kit/ui/styles/neutral/gray.css";
// any accent scales you want:
import "elements-kit/ui/styles/palette/mint.css";
import "elements-kit/ui/styles/accent/mint.css";
// and the toggle itself:
import "elements-kit/ui/toggle/toggle.css";

API

<!-- Binary toggle (checkbox) -->
<label class="x-toggle" data-variant="surface" data-size="2">
<input type="checkbox" class="unset" />
Bold
</label>
<!-- Exclusive group (radio, native name attribute) -->
<label class="x-toggle">
<input type="radio" name="align" class="unset" />
Left
</label>
<label class="x-toggle">
<input type="radio" name="align" class="unset" checked />
Center
</label>
<label class="x-toggle">
<input type="radio" name="align" class="unset" />
Right
</label>

The unset class on the input strips native chrome (Styles → Unset native styles). Visually only the label shows.

AttributeValues
data-variantsurface (default), soft, borderless
data-size1, 2 (default), 3
data-iconmakes the toggle a square (icon-only) — drops horizontal padding, locks aspect-ratio: 1
data-layoutstacked — icon above the label, same geometry as a stacked button; stacked-icon — no padding, the state on a highlight around the icon (see Stacked)
data-accent (on element or any ancestor)any imported color scale
disabled (on the wrapped input)greys out + stops clicks

Sizing

data-sizeheightpadding-xfont-size
1--space-5--space-2--font-size-1
2 (default)--space-6--space-3--font-size-2
3--space-7--space-4--font-size-3

Variants

The pressed state uses accent (--accent-*) tokens, which fall back to neutral when no data-accent is set. Pressing changes the fill and the text color, never the weight, so a toggle keeps its width. The fill steps are large enough to stay distinct from rest even when everything is gray.

VariantRestPressedPressed + hover
surface (default)--neutral-a5 ring, --neutral-a11 text--accent-a4 fill, --accent-a8 ring, --accent-a12 text--accent-a5
soft--neutral-a3 track, --neutral-a11 text--accent-a6 track, --accent-a12 text--accent-a7
borderlessno fill, --accent-a11 text--accent-a4 fill, --accent-a12 text--accent-a5
  • soft — the pressed track sits three alpha steps above rest, so it reads clearly even with a neutral accent.
  • borderless — at rest it matches a borderless button, so a toggle can sit among buttons in a toolbar or a material group. Only the pressed toggle shows a fill.

Set data-accent="iris" on the toggle or any ancestor to tint the pressed state.

Stacked

Add data-layout="stacked" to put the icon above the label. The geometry matches a stacked button: at size 2, a 24px icon, a 2px gap, and a medium-weight caption in --font-size-0 (10px) on a 16px line, in an item at least 56px square. The line scales with the size, and size 3 captions use --font-size-1.

<nav aria-label="Sections" style="display: flex; flex-direction: column; gap: 4px; width: 80px">
<label class="x-toggle" data-variant="borderless" data-layout="stacked">
<input type="radio" name="section" class="unset" checked />
<svg viewBox="0 0 24 24" aria-hidden="true">…</svg>
<span>Home</span>
</label>
<label class="x-toggle" data-variant="borderless" data-layout="stacked">
<input type="radio" name="section" class="unset" />
<svg viewBox="0 0 24 24" aria-hidden="true">…</svg>
<span>Photo Library</span>
</label>
</nav>

A stacked toggle widens for its label. When its container constrains the width, as in a navigation rail, the label wraps into balanced lines and the toggle grows taller.

Icon indicator

data-layout="stacked-icon" drops the item’s padding and paints the state on a highlight around the icon, not the whole item. At size 2 the highlight is a 36px square (the 24px icon with 6px around it) with the toggle’s radius, and the label sits 4px below, so the item stays 56px square. Both scale with the size.

Every variant works: the highlight takes its fill, ring and hover. Use borderless so only the selected item shows one.

<label class="x-toggle" data-variant="borderless" data-layout="stacked-icon">
<input type="radio" name="section" class="unset" checked />
<svg viewBox="0 0 24 24" aria-hidden="true">…</svg>
<span>Home</span>
</label>

Grouping

For an exclusive single-select group, give all the radio inputs the same name:

<div style="display: inline-flex; gap: 4px">
<label class="x-toggle"><input type="radio" name="align" class="unset" /> Left</label>
<label class="x-toggle"><input type="radio" name="align" class="unset" /> Center</label>
<label class="x-toggle"><input type="radio" name="align" class="unset" /> Right</label>
</div>

The kit deliberately doesn’t ship a .x-toggle-group class — flex + gap is one line.

For a joined-pill segmented control (no gaps between items, one continuous bar), use x-segmented-control instead.

States

  • :has(input:checked) — pressed. Drives the bg + color flip.
  • :hover — subtle wash on the bg.
  • :has(input:focus-visible) — 2px --focus-8 outline at +1px offset, on the label since the input is visually hidden. Keyboard focus only: a click doesn’t show it.
  • :has(input:disabled) — 0.6 opacity, pointer-events: none.

Accessibility

Native checkbox / radio semantics are intact — the input is visually hidden via off-screen positioning but remains in the tab order and announced by screen readers. Click events bubble through the label to the input, so space / enter toggles state via the browser default behavior. The visible focus outline lives on the label because the input itself has no visual.

See also

  • Segmented Control — joined single-select bar
  • Button — actions, and the stacked geometry toggles share
  • Toolbar — toggles in top and bottom bars
  • Group — material capsules for borderless toggles