Form

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

PartRoleSource
TrackFill, off--nm-switch-track-bg
TrackFill, on--nm-switch-track-checked-bg
TrackBorder--nm-switch-track-border / --nm-switch-track-checked-border — rendered as an inset box-shadow, not a real border
TrackSizeFixed 48×24px, padding: 2px, border-radius: 999px
ThumbFill, off--nm-switch-thumb-bg
ThumbFill, on--nm-switch-thumb-checked-bg
ThumbPositiontranslateX(0) off → translateX(24px) on → translateX(12px) middle
ThumbSizeFixed 20×20px
Check glyphColor--nm-switch-thumb-icon / --nm-switch-thumb-checked-icon
LabelTypography.nm-choice-field__label — 16px / 400 / Open Sans
Focus ringColor--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.

off
setting is off
on
setting is on
middle
mixed across a group

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.

Default
Hover
Read-only
Focus

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

Light data-theme="light"
Dark data-theme="dark"

Both previews are locked — they don't follow the page theme switcher.

Do & don't

✓Do
Use a switch for a setting that applies the instant it's flipped.
✕Don't
Don't put a switch inside a form with a "Save" button — that promises instant effect while making the user wait for submission.
✓Do
Label the switch with the setting's name, describing the state that's currently on.
✕Don't
Don't phrase the label as a question — the switch itself already asks and answers that.

Accessibility

Semantics

  • The underlying element is <input type="checkbox">. Add role="switch" on the input so assistive tech announces it as a switch rather than a generic checkbox, and mirror its state with aria-checked.
  • Wrap it in .nm-choice-field's <label> so the visible text is the accessible name.

Middle state

  • Set .is-indeterminate on the outer .nm-switch (not a native property here). Also set aria-checked="mixed" on the inner input so screen readers announce it correctly.

Read-only

  • Put .is-readonly on the outer .nm-switch and tabindex="-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-visible on 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

<label class="nm-choice-field"> <span class="nm-switch"> <input type="checkbox" class="nm-switch__input" checked> <span class="nm-switch__thumb"></span> </span> <span class="nm-choice-field__label">Auto-renew subscription</span> </label>

Token reference

TokenRole
--nm-switch-track-bgTrack fill, off
--nm-switch-track-borderTrack border, off
--nm-switch-track-checked-bgTrack fill, on
--nm-switch-track-checked-borderTrack border, on
--nm-switch-track-hover-bgTrack fill, hover
--nm-switch-track-hover-borderTrack border, hover
--nm-switch-track-checked-hover-bgTrack fill, hover + on
--nm-switch-track-readonly-bgTrack fill, read-only
--nm-switch-track-readonly-borderTrack border, read-only
--nm-switch-track-checked-readonly-bgTrack fill, read-only + on
--nm-switch-track-checked-readonly-borderTrack border, read-only + on
--nm-switch-thumb-bgThumb fill, off
--nm-switch-thumb-iconThumb glyph color, off (visible only when middle)
--nm-switch-thumb-checked-bgThumb fill, on
--nm-switch-thumb-checked-iconThumb glyph color, on
--nm-switch-thumb-hover-bgThumb fill, hover
--nm-switch-thumb-hover-iconThumb glyph color, hover
--nm-switch-thumb-checked-hover-bgThumb fill, hover + on
--nm-switch-thumb-checked-hover-iconThumb glyph color, hover + on
--nm-switch-thumb-readonly-bgThumb fill, read-only
--nm-switch-thumb-readonly-iconThumb glyph color, read-only
--nm-switch-thumb-checked-readonly-bgThumb fill, read-only + on
--nm-switch-thumb-checked-readonly-iconThumb 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

ClassApplies toValues
nm-switchOuter trackFixed 48×24px — no size modifiers
nm-switch__inputInner <input type="checkbox">Visually hidden, carries real focus/interaction
nm-switch__thumbSliding knobSibling of nm-switch__input, positioned via :checked
is-indeterminateOuter trackMiddle state — set manually, no native equivalent
is-readonlyOuter trackPair with tabindex="-1" / aria-readonly="true" on the input