Switch
A switch is an on/off control that represents a binary setting. <cinq-switch>
provides an accessible, keyboard-navigable interface following the
WAI-ARIA Authoring Practices switch pattern,
reads its state from the markup, syncs an optional hidden checkbox, and
dispatches events when toggled.
Inspired by @19h47/switch.
Install
Section titled “Install”pnpm add @agencecinq/switchImport once:
import "@agencecinq/switch";Then write the WAI-ARIA markup. The host <cinq-switch> is the focusable switch
element — set role="switch", aria-checked, and tabindex explicitly.
HTML is the source of truth. The component will not auto-set
role, auto-migrate attributes, or warn about missing labels. Run an a11y linter (axe, Lighthouse) to catch invalid markup.
<cinq-switch role="switch" aria-checked="false" tabindex="0"> <span class="label">Infravision</span> <span class="switch" aria-hidden="true"><span></span></span> <span class="on" aria-hidden="true">On</span> <span class="off" aria-hidden="true">Off</span> <div hidden> <input type="checkbox" name="infravision" tabindex="-1" aria-hidden="true" /> </div></cinq-switch>When disabled, use disabled or aria-disabled="true" and tabindex="-1":
<cinq-switch role="switch" aria-checked="false" disabled aria-disabled="true" tabindex="-1"> <span class="label">Cursed item</span> <span class="switch" aria-hidden="true"><span></span></span> <div hidden> <input type="checkbox" tabindex="-1" aria-hidden="true" disabled /> </div></cinq-switch>The component upgrades automatically on connectedCallback — no manual
initialization is needed.
Required markup
Section titled “Required markup”| Attribute / element | Required | Role |
|---|---|---|
<cinq-switch> |
Yes | Host element with role="switch". |
role="switch" |
Yes | Identifies the control as a switch. |
aria-checked |
Yes | Current on/off state ("true" or "false"). |
tabindex="0" |
Yes | Makes the switch keyboard-focusable (use -1 when disabled). |
| Accessible name | Yes | Via aria-label, aria-labelledby, or visible label text. |
Hidden input[type="checkbox"] |
Optional | Form helper only — not the switch. Keep it in a hidden container with tabindex="-1" and aria-hidden="true". |
aria-hidden="true" on On/Off text |
Recommended | Decorative state labels must not appear in the accessible name. |
Use [data-switch-input] instead of input[type="checkbox"] when you need a
more specific selector.
Per the APG switch pattern,
the switch label must not change when its state changes. Style On/Off
decorative text with CSS and mark it aria-hidden="true".
For multiple switches, use a <fieldset> / <legend> or role="group" with
aria-labelledby.
Markup variants
Section titled “Markup variants”The playground below covers every supported markup pattern:
| Variant | When to use |
|---|---|
| Visible label text | Default — text inside <cinq-switch> becomes the accessible name. |
aria-label |
Icon-only or compact switches with no visible label. |
aria-labelledby |
Name lives in a separate visible element (heading, table cell, etc.). |
aria-describedby |
Extra static help text announced with the switch. |
| Minimal | No slider, no checkbox — UI-only toggle outside a form. |
| Pre-checked | aria-checked="true" (+ optional checked for initial CSS). |
| Form helper | Hidden input[type="checkbox"] or [data-switch-input] for submission. |
| Switch group | <fieldset> / <legend> wrapping several switches. |
| Disabled | disabled + aria-disabled + tabindex="-1", or aria-disabled alone. |
aria-label
Section titled “aria-label”<cinq-switch role="switch" aria-checked="false" tabindex="0" aria-label="Battle cries"> <span class="slider" aria-hidden="true"></span></cinq-switch>aria-labelledby
Section titled “aria-labelledby”<span id="prefs-reduced-motion">Show THAC0</span>
<cinq-switch role="switch" aria-checked="false" tabindex="0" aria-labelledby="prefs-reduced-motion"> <span class="slider" aria-hidden="true"></span></cinq-switch>aria-describedby
Section titled “aria-describedby”<cinq-switch role="switch" aria-checked="false" tabindex="0" aria-describedby="push-desc"> <span class="label">Torch light</span> <span class="slider" aria-hidden="true"></span></cinq-switch>
<p id="push-desc">Keeps a dim light source active while the party explores underground.</p>Minimal (no form)
Section titled “Minimal (no form)”<cinq-switch class="autoplay-switch" role="switch" aria-checked="false" tabindex="0"> Auto-map corridors</cinq-switch>.autoplay-switch { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.5rem; color: #6b7280;}
.autoplay-switch[aria-checked="true"],.autoplay-switch[checked] { border-color: #111827; background-color: #111827; color: #fff; font-weight: 600;}Pre-checked
Section titled “Pre-checked”<cinq-switch role="switch" aria-checked="true" checked tabindex="0"> <span class="label">Sage's gazette</span> <span class="slider" aria-hidden="true"></span></cinq-switch>Set checked on the host in markup when you style with cinq-switch[checked] before the
first toggle. The component reflects checked automatically after state changes.
Switch group
Section titled “Switch group”<fieldset> <legend>Campaign preferences</legend>
<cinq-switch role="switch" aria-checked="false" tabindex="0"> <span class="label">Identify magic items</span> <span class="slider" aria-hidden="true"></span> </cinq-switch>
<cinq-switch role="switch" aria-checked="true" checked tabindex="0"> <span class="label">Show THAC0</span> <span class="slider" aria-hidden="true"></span> </cinq-switch></fieldset>[data-switch-input]
Section titled “[data-switch-input]”<cinq-switch role="switch" aria-checked="false" tabindex="0"> <span class="label">Henchman reports</span> <span class="slider" aria-hidden="true"></span> <div hidden> <input type="checkbox" data-switch-input name="newsletter" tabindex="-1" aria-hidden="true" /> </div></cinq-switch>Accessibility
Section titled “Accessibility”Accessible label is required
Section titled “Accessible label is required”The switch must have an accessible name. Use one of:
aria-label="…"on<cinq-switch>aria-labelledby="some-id"referencing a visible label- Visible label text inside the slotted content (when it provides the name)
The component does not check this — rely on axe-core / Lighthouse / your CI.
Keyboard support
Section titled “Keyboard support”| Key | Function |
|---|---|
Tab |
Moves keyboard focus to the switch (tabindex="0" in markup). |
Space |
Toggles between on and off when focused. |
Enter |
Toggles between on and off when focused (optional per APG). |
On focus / blur, a .focus class is toggled on the host so you can style
the focused state without relying solely on :focus:
cinq-switch.focus { outline: 2px solid currentColor; outline-offset: 2px;}Attributes
Section titled “Attributes”| Attribute | Required | Description |
|---|---|---|
checked |
No | Reflected state on the host — useful for styling (cinq-switch[checked]). |
disabled |
No | Observed — syncs the hidden checkbox and blurs focus when set externally. Pair with aria-disabled="true" and tabindex="-1" in markup. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
activate(emit?) |
Turns the switch on. Dispatches an event by default. |
deactivate(emit?) |
Turns the switch off. Dispatches an event by default. |
toggle() |
Toggles on/off. Returns false if disabled or a cancelable event was aborted. |
destroy() |
Detaches listeners when removing the element from the DOM. |
Properties
Section titled “Properties”| Property | Description |
|---|---|
checked |
Whether the switch is currently on. |
disabled |
Whether the switch is disabled. |
$input |
The optional hidden checkbox element. |
Events
Section titled “Events”| Event | Cancelable | Detail | Description |
|---|---|---|---|
switch:activate |
Yes | { el } |
Fired before the switch turns on. Cancel to abort. |
switch:deactivate |
Yes | { el } |
Fired before the switch turns off. Cancel to abort. |
import { EVENTS } from "@agencecinq/utils";
$switch.addEventListener(EVENTS.SWITCH_ACTIVATE, (event) => { console.log("activated", event.detail.el);});
$switch.addEventListener(EVENTS.SWITCH_DEACTIVATE, (event) => { console.log("deactivated", event.detail.el);});Call event.preventDefault() to abort before the state changes:
$switch.addEventListener(EVENTS.SWITCH_ACTIVATE, (event) => { if (!userMayEnable()) { event.preventDefault(); }});Programmatic API
Section titled “Programmatic API”const $switch = document.querySelector("cinq-switch");
$switch.activate();$switch.deactivate();$switch.toggle();$switch.destroy();activate() and deactivate() accept an optional emit flag (default true).
Pass false to update state without dispatching an event.
Examples
Section titled “Examples”Basic switch
Click or press Space / Enter to toggle. Activate and deactivate events are logged below.
Last event: —
On / Off labels
A larger track can reveal On and Off text inside the slider. Style with [aria-checked="true"] or the reflectedchecked attribute on the host.
State: off
Accessible naming
Per the APG, the switch needs an accessible name via visible text, aria-label, oraria-labelledby.
aria-labelNo visible label — name comes from aria-label.
aria-labelledbyThe visible heading above is referenced by aria-labelledby.
Minimal markup
No slider, no hidden checkbox — enough when you do not submit a form. The visible text inside the host provides the accessible name. Style the on state with[aria-checked="true"] or [checked].
Pre-checked state
Set aria-checked="true" in markup for an initially on switch. Addchecked on the host too if you style with cinq-switch[checked]— the component reflects checked after the first toggle.
Description
Use aria-describedby when additional static help text should be announced with the switch.
Keeps a dim light source active while the party explores underground.
Switch group
Wrap related switches in a <fieldset> with a<legend>, or use role="group" witharia-labelledby.
Form helper input
Use [data-switch-input] when several checkboxes live inside the host. Keep it hidden from assistive tech — the host is the switch.
Disabled
Pair disabled with aria-disabled="true" andtabindex="-1". The getter also respects aria-disabled alone.
disabled + aria-disabledaria-disabled onlyProgrammatic API
Call activate(), deactivate(), or toggle() from external controls.
State: off
Cancellable events
Activate and deactivate events fire before the state changes. Callevent.preventDefault() to abort — useful for confirmations or gating access.
Last attempt: —