Switch
An instant-apply on/off control that owns its label and its own busyness. A switch applies its effect at once; a checkbox collects a value for a later submit.
className styles the wrapper
Switch renders a wrapper around the control so it can hold the label. className styles that wrapper, not the track. Every other Radix prop passes through to the control. This matches Input and diverges from stock shadcn, which puts className on the track itself.
On and off
One size, no variants. Off is --muted-foreground as the track under a --background thumb; on is --indicator under an --indicator-foreground thumb. Every pair clears 3:1 against the page and against the thumb in both modes — 4.6:1 in light, 6.3:1 in dark — so the state is readable without color vision.
The off thumb takes the page color, not white. A white thumb in dark mode would have to turn near-black on the way to the on state, because --indicator-foreground is near-black there. Travel is a morph, so the thumb stays one object with one color across the whole journey. The page color gives that in both modes and raises the dark-mode contrast at the same time.
Instant apply
A switch applies at once, so the app owns the call. Control checked, set loading while the call runs, and revert checked if it fails. The example below confirms first: the thumb stays put until the call returns.
Loading
loading pulses the thumb and locks the toggle. Click, Space, and Enter are ignored, and a label click is ignored too. Focus stays put — the switch never leaves the tab order mid-action. The thumb sits where checked puts it, so you choose optimistic flip or confirm-first.
Disabled
Disabled dims the track and the label together and takes no pointer events. Use it when the setting cannot be changed at all; use loading when a change is in flight.
Without a label
Drop label when a table row or a section heading already names the setting. Pass aria-label so the control keeps an accessible name.
A failed toggle is a result
There is no error prop. A switch that fails to apply is an action result, not a field error, so the app reverts checked and shows the reason where the user is looking, on the switch's row or through alert. That is the feedback rule: the acting component shows its own busyness, the app shows the outcome, and the outcome stays until seen.
Motion
The thumb travels on spring-bounce in both directions through motion’s layout prop — travel is a morph, not an exit, so it bounces on the way back too. The track color crossfades under it in CSS at --motion-fast, as do the hover shade and the focus ring. The loading pulse is a continuous animation: the thumb color fades and returns on an 800ms CSS keyframes cycle, matched to the spinner’s tempo. The thumb never changes size, so it never reads as travel.
Accessibility
Switch generates the control id and wires the label htmlFor itself, so clicking the label toggles the setting. Tab reaches the control and shows a 3px ring on :focus-visible only. Space and Enter both toggle. A loading switch announces aria-disabled rather than aria-busy, which screen readers support poorly, and it never sets the disabled attribute, which would drop it from the tab order.