Toggle
A toggle switch applies a setting immediately — no form submission required.
Components · Form
A toggle (switch) flips a setting on or off immediately — there's no surrounding "Save" step. Namirial switches are built from --nm-switch-track-* and --nm-switch-thumb-* tokens defined in namirial-theme.css, ship at a single fixed size, and pair with the same .nm-choice-field label wrapper used by checkbox and radio. In the CSS the component is named .nm-switch; "toggle" is the name used in product copy and in this doc's navigation.
Overview
A toggle switch represents a binary on/off, active/inactive, show/hide state. If the setting has more than two states, or requires the user to pick from a list of options, use a different control (radio buttons, select, segmented control).
When to use
Instant effect:
A toggle change should apply immediately — the user should never need to click Save or Submit to confirm it. Exceptions:
- Inside a desktop dialog: if flipping the switch reveals more complex settings, it's okay for the whole change to apply together with the dialog's own "Save" or "Submit" — instead of taking effect the instant it's flipped
- Big or hard-to-undo changes: if flipping it does something serious (like turning off two-factor authentication), still use a switch — just ask for confirmation first, rather than applying it silently
Dependent behavior:
Show or hide a set of dependent settings tied to that state: Hide dependent settings when the toggle is off, and reveal them again when it's on — but preserve and save their values while hidden. Show/hide with the toggle explicitly if there isn't enough space to show all dependent settings at once, or when the UI overloads.
Toggle vs Checkbox:
A switch and a checkbox both represent a binary on/off state, but they carry different promises to the user: flipping a switch takes effect the instant it's flipped, while a checkbox is understood to be staged until the surrounding form is submitted.
Anatomy
A switch is a compound component: an outer track (.nm-switch), a visually-hidden native <input type="checkbox"> (.nm-switch__input) that carries all real interaction and focus, and a visible thumb (.nm-switch__thumb) that slides between the two ends of the track. The thumb's own ::after renders a small checkmark (or dash, when indeterminate) once it reaches the "on" position.
Tokens and values that drive each part
| Part | Role | Source |
|---|---|---|
| Track | Fill, off | --nm-switch-track-bg |
| Track | Fill, on | --nm-switch-track-checked-bg |
| Track | Border | --nm-switch-track-border / --nm-switch-track-checked-border — rendered as an inset box-shadow, not a real border |
| Track | Size | Fixed 48×24px, padding: 2px, border-radius: 999px |
| Thumb | Fill, off | --nm-switch-thumb-bg |
| Thumb | Fill, on | --nm-switch-thumb-checked-bg |
| Thumb | Position | translateX(0) off → translateX(24px) on → translateX(12px) middle |
| Thumb | Size | Fixed 20×20px |
| Check glyph | Color | --nm-switch-thumb-icon / --nm-switch-thumb-checked-icon |
| Label | Typography | .nm-choice-field__label — 16px / 400 / Open Sans |
| Focus ring | Color | --palette-focus-700 (light) / --palette-focus-300 (dark), 2px outline, 2px offset |
Selection states
Like checkbox, a switch supports a middle (indeterminate) state for summarizing a group of settings that aren't all in the same position. Unlike checkbox, this is a class on the outer track (.is-indeterminate) rather than a native DOM property, since the visible thumb position (centered) isn't something the native indeterminate property drives on its own here.
Switch is fixed at 48×24px (track) / 20×20px (thumb) — there are no size modifiers.
Interaction states
Hover targets the whole track, not just the thumb — this matches the larger click/tap target real switches have.
All shown in the "on" position
No disabled state
Switches never ship the native "greyed out" disabled look. If a setting is off the table for now, use read-only so its current position stays legible and its value still submits with the form.
Read-only implementation
Put .is-readonly on the outer .nm-switch track (that's what the CSS targets) and tabindex="-1" / aria-readonly="true" on the inner .nm-switch__input, since that's the real focusable element:
<span class="nm-switch is-readonly">
<input type="checkbox" class="nm-switch__input" checked tabindex="-1" aria-readonly="true">
<span class="nm-switch__thumb"></span>
</span>Markup
Pair with .nm-choice-field the same way as checkbox and radio. Write the label as the setting's name in its current framing, not as a question or an instruction to act.
Light & dark theme
Both previews are locked — they don't follow the page theme switcher.
Do & don't
Accessibility
Semantics
- The underlying element is
<input type="checkbox">. Addrole="switch"on the input so assistive tech announces it as a switch rather than a generic checkbox, and mirror its state witharia-checked. - Wrap it in
.nm-choice-field's<label>so the visible text is the accessible name.
Middle state
- Set
.is-indeterminateon the outer.nm-switch(not a native property here). Also setaria-checked="mixed"on the inner input so screen readers announce it correctly.
Read-only
- Put
.is-readonlyon the outer.nm-switchandtabindex="-1"/aria-readonly="true"on the inner.nm-switch__input— the real focusable element. This removes it from the tab order while keeping its value in the form.
Keyboard
- Tab moves focus to the hidden
<input>; Space toggles it. - Focus uses
:focus-visibleon the input, but the ring should visually wrap the track — verify it renders around the full.nm-switch, not just the invisible input, when you implement it.
Playground
Token reference
| Token | Role |
|---|---|
| --nm-switch-track-bg | Track fill, off |
| --nm-switch-track-border | Track border, off |
| --nm-switch-track-checked-bg | Track fill, on |
| --nm-switch-track-checked-border | Track border, on |
| --nm-switch-track-hover-bg | Track fill, hover |
| --nm-switch-track-hover-border | Track border, hover |
| --nm-switch-track-checked-hover-bg | Track fill, hover + on |
| --nm-switch-track-readonly-bg | Track fill, read-only |
| --nm-switch-track-readonly-border | Track border, read-only |
| --nm-switch-track-checked-readonly-bg | Track fill, read-only + on |
| --nm-switch-track-checked-readonly-border | Track border, read-only + on |
| --nm-switch-thumb-bg | Thumb fill, off |
| --nm-switch-thumb-icon | Thumb glyph color, off (visible only when middle) |
| --nm-switch-thumb-checked-bg | Thumb fill, on |
| --nm-switch-thumb-checked-icon | Thumb glyph color, on |
| --nm-switch-thumb-hover-bg | Thumb fill, hover |
| --nm-switch-thumb-hover-icon | Thumb glyph color, hover |
| --nm-switch-thumb-checked-hover-bg | Thumb fill, hover + on |
| --nm-switch-thumb-checked-hover-icon | Thumb glyph color, hover + on |
| --nm-switch-thumb-readonly-bg | Thumb fill, read-only |
| --nm-switch-thumb-readonly-icon | Thumb glyph color, read-only |
| --nm-switch-thumb-checked-readonly-bg | Thumb fill, read-only + on |
| --nm-switch-thumb-checked-readonly-icon | Thumb glyph color, read-only + on |
Implementation
The visible track and thumb are decorative; the real interactive element is the hidden checkbox input. No JavaScript is required for basic on/off — only for the middle state, which has no native equivalent.
<link rel="stylesheet" href="namirial-theme.css">
<label class="nm-choice-field">
<span class="nm-switch">
<input type="checkbox" class="nm-switch__input" role="switch">
<span class="nm-switch__thumb"></span>
</span>
<span class="nm-choice-field__label">Dark mode</span>
</label>CSS classes
| Class | Applies to | Values |
|---|---|---|
nm-switch | Outer track | Fixed 48×24px — no size modifiers |
nm-switch__input | Inner <input type="checkbox"> | Visually hidden, carries real focus/interaction |
nm-switch__thumb | Sliding knob | Sibling of nm-switch__input, positioned via :checked |
is-indeterminate | Outer track | Middle state — set manually, no native equivalent |
is-readonly | Outer track | Pair with tabindex="-1" / aria-readonly="true" on the input |