Switch

input.switch[type="checkbox"][role="switch"]

Switch — an immediate on/off control, built on <input type="checkbox" role="switch">.

Examples

On and off, immediately

View “On and off, immediately” as
<div class="field field-inline">
  <input class="switch" id="demo-wifi" name="wifi" type="checkbox" role="switch" checked />
  <label class="field-label" for="demo-wifi">Wi-Fi</label>
</div>
<div class="field field-inline">
  <input class="switch" id="demo-sync" name="sync" type="checkbox" role="switch" />
  <label class="field-label" for="demo-sync">Background sync</label>
</div>
<div class="field field-inline">
  <input class="switch" id="demo-locked" name="locked" type="checkbox" role="switch" checked disabled />
  <label class="field-label" for="demo-locked">Two-factor authentication — required by your organisation</label>
</div>

role="switch" is on the input. CSS cannot add a role, and a switch that announces itself as a checkbox is worse than one that looks like a checkbox.

A switch takes effect the moment it moves — if the change only lands when a form is submitted, it is a checkbox and should look like one.

The sizes, and why the small one is not shorter

View “The sizes, and why the small one is not shorter” as
<div class="field field-inline">
  <input class="switch switch-sm" id="size-switch-sm" type="checkbox" role="switch" checked />
  <label class="field-label" for="size-switch-sm">.switch-sm — a narrower track, still 24px tall</label>
</div>
<div class="field field-inline">
  <input class="switch" id="size-switch-md" type="checkbox" role="switch" checked />
  <label class="field-label" for="size-switch-md">Default</label>
</div>
<div class="field field-inline">
  <input class="switch switch-lg" id="size-switch-lg" type="checkbox" role="switch" checked />
  <label class="field-label" for="size-switch-lg">.switch-lg</label>
</div>

The small variant narrows the track and keeps its height, because 24px is the WCAG 2.2 target-size floor and a switch that shrank in both directions would fail it.

Shown side by side like this the result reads as a rendering mistake, which is the argument for using .switch-sm where it is alone in its context: a table row, a toolbar, a dense settings panel. In forced-colours mode all three states are drawn with Highlight/HighlightText, because a switch carries its state in a fill and a position and forced colours flattens the fill.

Tokens 13

Level 2, declared on .switch itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by switch
TokenDefault
--switch-bgvar(--color-surface-sunken)
--switch-block-size1.5rem
--switch-border-colorvar(--color-border-strong)
--switch-border-widthvar(--border-width)
--switch-checked-bgvar(--color-primary)
--switch-checked-border-colorvar(--color-primary)
--switch-checked-thumb-bgvar(--color-on-primary)
--switch-inline-size2.75rem
--switch-invalid-border-colorvar(--color-danger)
--switch-padding0.1875rem
--switch-radiusvar(--radius-full)
--switch-thumb-bgvar(--color-border-strong)
--switch-thumb-radiusvar(--radius-full)

Variants and states

Variants

  • .switch-lg
  • .switch-sm

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :checked
  • :disabled
  • :focus
  • :hover
  • :user-invalid

Before you ship it

What you have to do 1 requirements

  1. Set role="switch". CSS cannot add a role, and a switch-looking checkbox that announces itself as a checkbox is worse than one that looks like a checkbox. Label it like any other control. If flipping it triggers work that is not instant, say so — aria-busy, or a status region — because the thumb moving is a visual claim that the change already happened.

src/css/components/switch.css · npx mostlycss add switch