Skip to content

Switch

A binary on/off control. It takes effect immediately — flipping it does something right away.

Use a Checkbox instead when the value is collected now and applied later, as part of a form the user submits. “Enable dark mode” is a switch. “I agree to the terms” is a checkbox. If your control sits next to a Save button, it is probably a checkbox.

Props
<script setup lang="ts">
import { Switch } from '@caioalfonso/kanso-vue';
import { ref } from 'vue';
const checked = ref(false);
</script>
<template>
<Switch v-model="checked" label="Wi-Fi" />
</template>

The two panels above are different libraries rendering the same core. Every keyboard interaction, every ARIA attribute and every state transition comes from the same @caioalfonso/kanso-core code; only the rendering differs.

import { Switch } from '@caioalfonso/kanso-react/switch';
import '@caioalfonso/kanso-styles/tokens';
import '@caioalfonso/kanso-styles/base';
import '@caioalfonso/kanso-styles/switch';

The stylesheet is optional — without it you get a fully functional, entirely unstyled switch. See Installation for package setup, the Vue entry points, and why tokens and base come along.

PartElementNotes
root<span>Carries data-kanso and data-scope=“switch”. The only marked element.
control<button>The track. Holds role=“switch” and every interaction.
thumb<span>Moves. Position is the primary state cue, not colour.
label<label>Rendered only when label is given.
hidden-input<input>Internal. Rendered only when name is set, for form submission.

Every part is addressable as [data-part="…"], and stateful parts also carry data-state="checked" | "unchecked". That is the whole styling contract — there are no class names to depend on.

The list above comes from switchAnatomy, which the library exports: {switchAnatomy.join(', ')}.

PropTypeDefaultNotes
checkedbooleanControlled. Present ⇒ you own the value.
defaultCheckedbooleanfalseUncontrolled initial value.
onCheckedChange(checked: boolean) => voidFires in both modes. Vue emits checkedChange.
disabledbooleanfalseNot focusable, not toggleable.
readOnlybooleanfalseFocusable, announced, but refuses writes.
requiredbooleanfalseOnly meaningful with name.
namestringSet it to make the switch submit with a form.
valuestring'on'Submitted value when checked.
idstringautoLands on the root verbatim; other ids derive from it.
labelstringRequired for accessibility unless you pass aria-label.

Vue additionally supports v-model, which maps onto the same core events.

KeyAction
SpaceToggle
EnterToggle
TabMove focus to and from the switch

Both toggle keys come free from the native <button> underneath. The library adds no keydown handler — hand-rolling keyboard support for a control the platform already implements is how you end up with a switch that double-toggles.

Follows the APG switch pattern.

  • role="switch" and aria-checked sit on the <button>, never on the root.
  • aria-labelledby points at the rendered <label> — and is omitted entirely when no label is rendered. An idref pointing at an element that does not exist leaves the control with no accessible name at all, which is worse than having no attribute.
  • readOnly sets aria-readonly and keeps the control focusable. disabled uses the native attribute rather than aria-disabled.
  • State is signalled by position first. The thumb moves; colour reinforces. The switch is still readable in monochrome, and the track border is measured at ≥ 3:1 against every surface it can sit on (WCAG 2.2 SC 1.4.11).
  • The focus ring is :focus-visible only — never on a mouse click, always on keyboard.

Set name and the switch renders a visually-hidden <input type="checkbox"> that mirrors its state, so it works inside a plain <form> with no JavaScript on your side:

<form action="/settings" method="post">
<Switch label="Email me" name="notify" defaultChecked />
<button type="submit">Save</button>
</form>

That input is aria-hidden and out of the tab order, so it never becomes a second control. It is clipped, not display: none — a control hidden by display is skipped by constraint validation, which would silently disable required.

const [checked, setChecked] = useState(false);
<Switch label="Wi-Fi" checked={checked} onCheckedChange={setChecked} />
{/* Focusable and announced. Shows a value you may not change. */}
<Switch label="Managed by your administrator" readOnly checked />
{/* Removed from the tab order entirely. */}
<Switch label="Unavailable" disabled />

Prefer readOnly when the value carries information. A disabled control is skipped by screen reader users navigating by form field, so its state may never be announced at all.

The library ships no styles unless you import them. Skip the stylesheet and target the data attributes yourself:

[data-scope='switch'] [data-part='control'] {
/* your track */
}
[data-scope='switch'] [data-part='thumb'][data-state='checked'] {
/* your checked thumb */
}