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> |
Optional | Form helper only, not the switch. One input in the host. 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. |
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> for form 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>Form helper input
Section titled “Form helper 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" 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="Battle cries"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). |
Style focus with native pseudo-classes:
cinq-switch:focus-visible { 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 |
|---|---|
init() |
Binds markup + listeners. Call destroy() first if already bound. |
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();After mutating light DOM, call destroy() then init():
$switch.destroy();// mutate light DOM...$switch.init();activate() and deactivate() accept an optional emit flag (default true).
Pass false to update state without dispatching an event. The Programmatic API
playground section includes a silent sync button.
Form submission
Section titled “Form submission”The hidden checkbox stays in sync with aria-checked. Submit the form to read values
with FormData (see Form submission in the playground).
Advanced settings panel
Section titled “Advanced settings panel”The Campaign settings (advanced) playground demo combines:
- form submit with multiple switches
- a master switch that enables/disables a child via
disabled+tabindex - preset loading with
activate(false)(no events) - locking the whole panel with an external
disabledattribute
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 reflected checked attribute on the host.
State: off
Accessible naming
Per the APG, the switch needs an accessible name via visible text, aria-label, or aria-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. Add checked 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" with aria-labelledby.
Form helper input
Optional hidden <input> for form submission (one per host). Keep it hidden from assistive tech. The host is the switch.
Form submission
Submit the form to read checkbox values synced by the component. The host stays the accessible switch. The hidden input is for the form only.
Submitted:-
Disabled
Pair disabled with aria-disabled="true" and tabindex="-1". The getter also respects aria-disabled alone.
disabled + aria-disabledaria-disabled onlyProgrammatic API
Call activate(), deactivate(), or toggle() from external controls.
State: off
Events:none(silent sync skips them)
Cancellable events
Activate and deactivate events fire before the state changes. Call event.preventDefault() to abort. Useful for confirmations or gating access.
Last attempt: -
Campaign settings (advanced)
Form submit, a master switch that enables/disables a child, preset loading viaactivate(false), and external disabled on the whole panel.
Toggle house rules, load a preset, or submit the form.