Switch

An on or off setting that takes effect right away. Use a Checkbox when the change waits for a Save button.

Accessibility

What Switch does for you, and what it leaves to the page around it.

What it renders

Switch is one element: <button type="button" role="switch" aria-checked>. There is no input, no label element and no hidden checkbox, and the kit adds none.
  • aria-checked follows the state on every render, so on and off are announced whether or not the track has any text.
  • disabled is the native attribute on the button, so a disabled switch leaves the tab order and ignores clicks.
  • It is one tab stop, and tabIndex passes through if you need to change it.
  • Focus shows as a :focus-visible outline in colorPrimaryBorder. color changes that token too, so a colored switch shows a focus ring in its own color. Keep the ring when you restyle.
The name and the contrast are left to you, because the component cannot decide them.

Keyboard

A native button handles Space and Enter, and Switch adds two arrow keys.
  • Space and Enter toggle, because the element is a button.
  • ArrowLeft turns the switch off and ArrowRight turns it on. They set a state rather than toggling, so holding one down is safe.
  • The arrows call onChange even when the state does not change: pressing left on a switch that is already off still fires it with false. If your handler writes to a server, check that the value changed.
  • The arrows do not fire onClick, while Space and Enter do. Put the work in onChange so every key behaves the same.

Naming it

The button has no name of its own. Give it one with aria-label, or point aria-labelledby at the visible text beside it.
  • A wrapping <label> makes its text a click target and also the switch's name, description included. Point aria-labelledby at the title and aria-describedby at the description, so the name stays short and the description is read after it.
  • A tooltip is not a label. It shows on hover and focus, not on touch, and a reader scanning a column of settings should not have to hover each one.
  • The title prop reaches the button, so it gives a browser tooltip and a fallback name. It is the weakest source of a name and only appears when the pointer rests on the switch, so use it for an aside, not for the name.
  • Name the setting, not the action or the state: aria-label="Dark mode", not “Turn dark mode on”. aria-checked already announces the rest.
  • When the setting needs explaining, put a description next to the label and keep the name short. A long name is read in full every time.

Announcing the state

aria-checked carries the state, so the track needs no text for a screen reader. The risk runs the other way: text inside the button can end up in its name.
  • checkedChildren and unCheckedChildren are both in the button at all times, with the inactive one moved out of view rather than hidden from screen readers. A switch with ON and OFF has both words inside it, and a name taken from its content is both of them.
  • The loading spinner is an icon labeled “loading”, so a spinning switch has that word inside it too.
  • An explicit aria-label or aria-labelledby wins over the content, so the name stays the same through every flip and every pending request.
  • Nothing announces the result of a toggle. If the request can fail, or the change affects something off screen, say so in a live region or a message.

Disabled and loading

loading also sets the native disabled attribute, so a spinning switch is a disabled switch.
  • A switch that starts loading while focused loses focus to the page, and the reader has to tab back to it. Keep pending states short, or move focus back yourself.
  • Tab and most screen-reader element lists skip a disabled control, so an explanation attached only to it cannot be reached. Put the reason in the row's own text, like a Pro Tag or a description line.
  • Disabled and loading look the same apart from the spinner, and both are drawn at lower opacity, which lowers contrast. Do not make a permanently disabled row the only place a value is shown.

Contrast in both modes

Three colors decide whether a switch can be read: the on track is colorPrimary, the off track is colorTextQuaternary, and the knob is white in light and dark mode.
  • The off state is the risky one. colorTextQuaternary is a faint token, and it is all that separates an off switch from the surface behind it. Check it against your page background in both modes.
  • The knob relies on handleShadow to stand out on a light track. A theme that flattens shadows can leave a white knob on a pale off track with nothing between them.
  • These values come from your theme, so contrast is a property of the theme. A custom colorPrimary picked for its look can land under 3:1 against the surface with no warning.
  • The same goes for every color value. Check yellow-leaning hues (gold, lime, amber) first, where the white knob is the weakest pair.
  • The knob's position is the only other difference between the states, and it is a few pixels. Where the state has to be read at a glance, or by someone who cannot tell the two colors apart, add checkedChildren and unCheckedChildren.

Motion

Switch animates the track color, the knob's position and the text inside the track, over motionDurationMid.
  • It follows the OS reduced-motion setting on its own: a reader who asked for reduced motion gets an instant flip.
  • To turn motion off for everyone, set theme={{ token: { motion: false } }}, and the switch snaps instead of sliding.
  • The focus outline has no transition, so it appears the moment the switch is focused.