VisorVisor
ComponentsForm

Switch

An accessible toggle switch for boolean settings. Built on Radix UI.

Default

Sizes

Checked

Disabled

Theming

A theme sets the default size's track and knob through the switch component tokens. knob-inset places the knob that far from the track's outer edge on every side and sizes it from the track height. See Component tokens.

components:
  switch:
    track-width: "30px"
    track-height: "17px"
    knob-inset: "3px"

The track's edge comes from the shared control tokens, so edges: off removes it with every other control's edge.

The knob and the hovered track have colour keys of their own. Each falls back to the token it has always read, so a theme that sets none renders exactly as before.

KeyTokenFalls back to
knob-bg--switch-knob-bgvar(--surface-page, #ffffff)
knob-bg-checked--switch-knob-bg-checkedvar(--surface-page, #ffffff)
track-hover-bg--switch-track-hover-bgvar(--border-strong, #d1d5db)
track-hover-bg-checked--switch-track-hover-bg-checkedvar(--border-strong, #d1d5db)

A dark, borderless theme needs them. Its page colour is black, so the unchecked knob is a black disc on a dark track, below the 3:1 that WCAG 1.4.11 asks of a control. Its --border-strong is transparent, so the track disappears under the pointer. Bind the knob and the hover fills in dark mode:

edges: "off"
components:
  switch:
    track-bg:
      dark: "#383838"
    knob-bg:
      dark: "#d4d4d4"
    track-hover-bg:
      dark: "#4a4a4a"
    track-hover-bg-checked:
      dark: "var(--interactive-primary-bg)"

knob-bg-checked does not follow knob-bg: a light unchecked knob and the default knob on a bright checked track can sit side by side.

Installation

npx visor add switch

This copies two files into your project:

  • components/ui/switch/switch.tsx — the component
  • components/ui/switch/switch.module.css — the styles

Usage

import { Switch } from '@/components/ui/switch/switch';
import { Label } from '@/components/ui/label/label';

<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
  <Switch id="airplane" />
  <Label htmlFor="airplane">Airplane mode</Label>
</div>

API Reference

SwitchProps

PropTypeDefaultDescription
checkedboolean—Controlled checked state.
defaultCheckedboolean—Default checked state for uncontrolled usage.
onCheckedChange(checked: boolean) => void—Callback when the checked state changes.
size'default' | 'sm''default'Size of the switch.
disabledbooleanfalseDisables the switch.
classNamestring—Additional CSS class names to merge onto the element.

The component also accepts all standard <button> HTML attributes (Radix renders switch as a button).

Accessibility

  • Built on @radix-ui/react-switch with full WAI-ARIA switch semantics
  • Renders with role="switch" and aria-checked reflecting the current state
  • Always pair with a <Label> using matching id/htmlFor — the label text communicates what the switch controls
  • Keyboard: Space or Enter toggles the switch; Tab moves focus to/from it
  • Use past-tense or noun labels ("Notifications", "Dark mode") rather than action labels ("Enable notifications")