Starter Docs
UI

Switch

A native checkbox drawn as a switch. Pressing stretches the thumb toward where it is going; releasing springs it across. Every animation is CSS.

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/switch

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/switch.json

The files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.

Usage

Installed from the registry, import from @/components/ui/switch; inside this monorepo, from @rabtx/ui/switch.

import { Switch } from "@rabtx/ui/switch";

export function WifiSetting() {
  const [on, setOn] = useState(true);
  return (
    <Switch checked={on} onCheckedChange={setOn}>
      Wi-Fi
    </Switch>
  );
}

It is an <input type="checkbox"> with role="switch" and the HTML switch attribute, so the browser provides Space to toggle, the form value (name, value), and the on and off state for screen readers. With children it wraps itself in a <label>, so the text names it and the whole row toggles it. A bare switch needs a Field or an aria-label.

States

Off, on and disabled. A disabled switch is dimmed to 50%, is skipped by Tab and never moves.

Field

Inside a Field, the label names the switch, the hint describes it, and error marks it invalid (a red edge and a soft halo) and links the message.

At most one a month.

Turn this on to continue.

Controlled

checked and onCheckedChange, as in shadcn. onChange still receives the native event.

Notifications are on

Settings rows

Reverse the label with flex-row-reverse justify-between to put the switch at the end of a row. The row is still one label, so a click anywhere on it toggles the switch.

Right to left

The thumb starts on the inline start edge and travels toward the end, so it mirrors in right-to-left text with no prop.

API

Switch takes everything a native checkbox <input> takes, except type, size and role, plus:

Prop

Type

Outside React, the class names are exported from @rabtx/ui/switch/styles:

<label class="rx-switch-label">
  <input type="checkbox" role="switch" switch class="rx-switch rx-switch-md" />
  Wi-Fi
</label>

Theming

The Switch reads only the theme tokens.

PartTokens
Trackoff --muted-foreground (70% over --background), on --primary
Thumboff --background, on --primary-foreground
Depth (Floating)track --field-edge-top, --field-edge-bottom, --field-shade; thumb --surface-ring, --surface-sheen, --surface-shade
Focus and error--ring, --destructive
Label--foreground, disabled --disabled-foreground
Corners and motion--radius-button, --spring-switch, --motion-ease

How the motion works

EffectCSS
Travel:checked sets the thumb's --_x to the track width minus its height; inset-inline-start follows on the switch spring (stiffness 800, damping 80, mass 4: a heavy thumb, about 4% overshoot, settled in 527ms), solved into a linear() easing
Press:active (on the switch or its label) widens the thumb by 4px toward where it is going (the near edge stays put) and scales it to 0.9, on the same spring
Releasethe toggle moves --_x while the width and scale spring back, so the thumb lands as it un-squishes
Track--_fill is a registered @property color, so the fill inside the gradient layers fades in 200ms
Right to leftthe thumb is placed with inset-inline-start (which is what transitions), so it starts and travels from the inline start in either direction

The thumb is the input's ::before, so there is no extra markup to keep in sync.

Browser support

  • Everywhere: appearance: none, :checked, :active, :has(), color-mix(), logical insets, nesting, @property and linear() easings: Chrome 120+, Safari 17.2+ and Firefox 128+ draw the same switch (from compatibility data; tested in Chrome).
  • Safari 17.4+: the switch attribute makes it a native switch (announced as on and off, :indeterminate never matches). The page's CSS still draws it. Elsewhere the attribute is ignored and role="switch" gives the same semantics.
  • Not used yet: the experimental ::thumb and ::track pseudo-elements (Safari behind a flag).

Accessibility

  • Announced as a switch, on or off, from the input's native checked state; there is no aria-checked to drift.
  • Space toggles it. Enter does not, as with any native checkbox.
  • Focus shows a 2px --ring outline. Label it with children, a Field or an aria-label.
  • The state is shown by position and color, never motion alone. Reduced motion and data-motion="off" switch instantly.

On this page