BetaComponent · since 0.4.0
An on/off switch built on a native checkbox with role="switch": the setting applies as soon as it changes.
Pick this part
Pick c-toggle for
- A setting that takes effect immediately, with no save or submit button: notifications, dark mode, a feature flag.
- A system state people expect to flip back and forth.
Pick another part for
- A choice inside a form that is saved on submit: use
c-checkbox. - Choosing between two named options that are not on/off, such as "Monthly" and "Yearly": use
c-radio. - Triggering an action, such as "Export": use
c-button.
Build it
- Start with
<div>and addclass="c-toggle". - Add the required parts:
c-toggle__input,c-toggle__label. - Add the parts you need:
c-toggle__hint. - Set state on the native control, never with data-state:
:checked,:disabled. - Add
data-state="loading"when the change is being saved. Setaria-busy="true"on thec-toggleand disable the input until the save finishes.
Applies straight away.
Show markup 15 lines
<div class="l-stack l-stack--sm">
<div class="c-toggle">
<input class="c-toggle__input" type="checkbox" role="switch" id="setting-email" checked aria-describedby="setting-email-hint">
<label class="c-toggle__label" for="setting-email">Email notifications</label>
<p class="c-toggle__hint" id="setting-email-hint">Applies straight away.</p>
</div>
<div class="c-toggle">
<input class="c-toggle__input" type="checkbox" role="switch" id="setting-digest">
<label class="c-toggle__label" for="setting-digest">Weekly digest</label>
</div>
<div class="c-toggle" data-state="loading" aria-busy="true">
<input class="c-toggle__input" type="checkbox" role="switch" id="setting-beta" checked disabled>
<label class="c-toggle__label" for="setting-beta">Beta features</label>
</div>
</div>
Check your build
Each piece that doesn't fit was run through mcss-lite validate when this page was built. The output below is real.
Fits
Build the switch on a real checkbox with role="switch" and a fixed noun label.
Applies straight away.
Show markup 5 lines
<div class="c-toggle">
<input class="c-toggle__input" type="checkbox" role="switch" id="notify" checked aria-describedby="notify-hint">
<label class="c-toggle__label" for="notify">Email notifications</label>
<p class="c-toggle__hint" id="notify-hint">Applies straight away.</p>
</div>
validate: 0 issues
Doesn't fit
Don't fake the on state with data-state: the control has to carry it.
<div class="c-toggle" data-state="checked">
<input class="c-toggle__input" type="checkbox" role="switch" id="notify-fake">
<label class="c-toggle__label" for="notify-fake">Email notifications</label>
</div>
validate
- error "checked" is a native state of c-toggle: don't use data-state. Use the checked attribute; role="switch" makes screen readers say on and off.
[invalid-state]
Fits
While a change saves, mark the switch busy and disable the input.
Show markup 4 lines
<div class="c-toggle" data-state="loading" aria-busy="true">
<input class="c-toggle__input" type="checkbox" role="switch" id="beta" checked disabled>
<label class="c-toggle__label" for="beta">Beta features</label>
</div>
validate: 0 issues
Doesn't fit
Don't show the loading stripes without aria-busy; screen readers can't tell the save is still running.
<div class="c-toggle" data-state="loading">
<input class="c-toggle__input" type="checkbox" role="switch" id="beta-x" checked disabled>
<label class="c-toggle__label" for="beta-x">Beta features</label>
</div>
validate
- warning data-state="loading" also needs aria-busy="true".
[state-pair]
Spec sheet
Apply to <div>
| Class | On | What it does |
|---|---|---|
c-toggle__input required | <input> | The native control: <input type="checkbox" role="switch">. Carries checked and disabled. |
c-toggle__label required | <label> | The name of the setting, linked with for/id. It stays the same when the switch flips. |
c-toggle__hint | <p> <div> <span> | Optional one-line hint under the label, linked with aria-describedby. |
| State | Meaning | Pair it with |
|---|---|---|
:checked | On: the thumb slides to the end on an ink track and shows a tick. | Use the checked attribute; role="switch" makes screen readers say on and off. |
:disabled | Cannot be changed. Dashed track, ghosted label. | Use the disabled attribute, and say nearby why it is unavailable. |
data-state="loading" | The change is being saved. The track turns neutral with moving stripes. | Set aria-busy="true" on the c-toggle and disable the input until the save finishes. |
Show tokens 6
| Token | Value (light / dark) |
|---|---|
--toggle-track-off | #e9ecef / #27436f |
--toggle-track-on | #1a1d20 / #e6edf4 |
--toggle-thumb | #ffffff / #14294d |
--toggle-border | #1a1d20 / #cfdae5 |
--toggle-mark | #1a1d20 / #e6edf4 |
--color-action-loading-stripe | rgb(0 0 0 / 0.14) / rgb(0 0 0 / 0.22) |
Accessibility
- Use <input type="checkbox" role="switch">: screen readers that know switches say "on" and "off", and the rest still say "checked".
- The thumb shows a tick when on and stays empty when off, so state never relies on position or color alone.
- Movement stops under prefers-reduced-motion.
| Key | What it does |
|---|---|
| Tab | Moves focus to the switch. |
| Space | Turns the setting on or off. |
Wording
- Toggle labels are nouns naming the setting, in sentence case: "Dark mode", not "Enable dark mode".
- Never change the label when the state changes.