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.
<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>import { Switch } from '@caioalfonso/kanso-react';import { useState } from 'react';
export function SwitchBasicReact() { const [checked, setChecked] = useState(false);
return <Switch label="Wi-Fi" checked={checked} onCheckedChange={setChecked} />;}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
Section titled “Import”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.
Anatomy
Section titled “Anatomy”| Part | Element | Notes |
|---|---|---|
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(', ')}.
| Prop | Type | Default | Notes |
|---|---|---|---|
checked | boolean | — | Controlled. Present ⇒ you own the value. |
defaultChecked | boolean | false | Uncontrolled initial value. |
onCheckedChange | (checked: boolean) => void | — | Fires in both modes. Vue emits checkedChange. |
disabled | boolean | false | Not focusable, not toggleable. |
readOnly | boolean | false | Focusable, announced, but refuses writes. |
required | boolean | false | Only meaningful with name. |
name | string | — | Set it to make the switch submit with a form. |
value | string | 'on' | Submitted value when checked. |
id | string | auto | Lands on the root verbatim; other ids derive from it. |
label | string | — | Required for accessibility unless you pass aria-label. |
Vue additionally supports v-model, which maps onto the same core events.
Keyboard
Section titled “Keyboard”| Key | Action |
|---|---|
Space | Toggle |
Enter | Toggle |
Tab | Move 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.
Accessibility
Section titled “Accessibility”Follows the APG switch pattern.
role="switch"andaria-checkedsit on the<button>, never on the root.aria-labelledbypoints 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.readOnlysetsaria-readonlyand keeps the control focusable.disableduses the native attribute rather thanaria-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-visibleonly — 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.
Examples
Section titled “Examples”Controlled
Section titled “Controlled”const [checked, setChecked] = useState(false);
<Switch label="Wi-Fi" checked={checked} onCheckedChange={setChecked} />Read-only versus disabled
Section titled “Read-only versus disabled”{/* 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.
Unstyled
Section titled “Unstyled”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 */}