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/theme.css";import "elements-kit/ui/styles/scaling.css";import "elements-kit/ui/styles/radius.css";import "elements-kit/ui/styles/space.css";import "elements-kit/ui/styles/typography.css";import "elements-kit/ui/styles/cursor.css";import "elements-kit/ui/styles/unset.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.
| Attribute | Values |
|---|---|
data-variant | surface (default), soft |
data-size | 1, 2 (default), 3 |
data-icon | makes the toggle a square (icon-only) β drops horizontal padding, locks aspect-ratio: 1 |
data-accent (on element or any ancestor) | any imported color scale |
disabled (on the wrapped input) | greys out + stops clicks |
Sizing
data-size | height | padding-x | font-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 rest state uses neutral tokens; the pressed state uses accent (--accent-*) tokens that fall back to neutral via accent/neutral.css when no data-accent is set. Both variants stack three hue-independent signals β alpha step, ring/weight, and font-weight: 500 β 600 β so pressed stays clearly distinct from rest even in the all-gray case.
surface(default) β thin--neutral-a5ring + muted--neutral-a11text at rest. Pressed: soft--accent-a5fill + stronger--accent-a8ring +--accent-12text. Hover bumps the pressed bg one alpha step deeper (--accent-a6).softβ--neutral-a3tinted track at rest. Pressed:--accent-a5accent-tinted track +--accent-12text.
Set data-accent="iris" on the toggle or any ancestor to tint the pressed state. Without an accent, the pressed state stays gray (just deeper than rest) β still clearly active.
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.:focus-withinβ2px--focus-8outline at+1pxoffset (on the label, since the input is visually hidden).:has(input:disabled)β0.6opacity,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.