Switch
Free · MITiOS, Material 3, square, neo-brutalist, icon thumb and ON/OFF label variants with none, smooth, spring and bounce motion, six tones, three sizes, track shadows, sun/moon or check/x glyphs, label and description on either side, and a loading state.
Installation
$ npx shadcn@latest add https://goodui.dev/r/switch.jsonThe file lands in components/goodui/switch.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/switch works too.
Usage
import * as React from "react"
import { Switch } from "@/components/goodui/switch"
export function ReminderSettings() {
const [sms, setSms] = React.useState(true)
const [saving, setSaving] = React.useState(false)
const toggleSms = async (next: boolean) => {
setSaving(true)
await fetch("/api/settings", { method: "PATCH", body: JSON.stringify({ smsReminders: next }) })
setSms(next)
setSaving(false)
}
return (
<div className="flex max-w-md flex-col gap-4">
<Switch
checked={sms}
onCheckedChange={toggleSms}
loading={saving}
label="SMS reminders"
description="Clients get a text 24 hours before their appointment."
labelPosition="left"
/>
<Switch defaultChecked label="Online booking" labelPosition="left" tone="success" name="onlineBooking" />
</div>
)
}When to use
- Settings that take effect immediately: SMS reminders on, online booking off, auto-renew.
- A settings list where each row has a title, a description and the toggle at the end: labelPosition="left".
- Toggles that save to a server: loading shows a spinner in the thumb and blocks extra clicks.
- Choices that only apply after the user presses Save, or agreeing to terms: use checkbox.
- Picking one of several options: use tabs, segmented-control or a RadioGroup from checkbox.
- A single light/dark button in a header: use theme-toggle.
| If you need | Use |
|---|---|
| Default app settings | variant="ios" (default) with animation="spring" |
| Android-like or Material apps | variant="material" (the thumb grows when on) |
| State that must be readable without color | variant="icon-thumb" (check / x) or "label-inside" (ON / OFF) |
| Theme switch in a settings page | icons="theme" (sun / moon in the thumb) |
| Retro or playful brands | variant="neo" or "square" |
| Calm, no-bounce motion | animation="smooth" or "none" |
| Settings row | label, description and labelPosition="left" |
Examples
ON / OFF in the track
<Switch variant="label-inside" defaultChecked aria-label="Auto-renew" />Translated track text
<Switch variant="label-inside" onText="AN" offText="AUS" label="Erinnerungen" />Theme switch
<Switch icons="theme" variant="material" size="lg" checked={dark} onCheckedChange={setDark} aria-label="Dark mode" />API reference
| Prop | Type | Default |
|---|---|---|
| defaultChecked Initial state when uncontrolled. | boolean | false |
| name Submits "on" (or `value`) under this name in a form when checked. | string | — |
| checked On state (controlled). | boolean | — |
| onCheckedChange Called with the new state on click, Space or Enter (not while loading). | (checked: boolean) => void | — |
| variant Track and thumb style: ios (default), material, square, neo, icon-thumb (check/x) or label-inside (ON/OFF). | "ios" | "material" | "square" | "neo" | "icon-thumb" | "label-inside" | "ios" |
| tone Track color when on. | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "primary" |
| animation How the thumb travels: spring (default), smooth ease, bounce with squash, or none. | "none" | "smooth" | "spring" | "bounce" | "spring" |
| shadow Track shadow; glow only shows when on. | "none" | "sm" | "inner" | "glow" | "none" |
| size | "sm" | "md" | "lg" | "md" |
| icons Glyphs inside the thumb. "auto" shows check/x on icon-thumb and nothing elsewhere. | "auto" | "none" | "check" | "theme" | "auto" |
| checkedIcon Custom thumb glyphs, override `icons`. | React.ReactNode | — |
| uncheckedIcon Custom thumb glyph for the off state. | React.ReactNode | — |
| onText Text inside the track for the label-inside variant. | string | "ON" |
| offText Off text for the label-inside variant. | string | "OFF" |
| label Visible label; the row becomes a <label> linked with aria-labelledby. | React.ReactNode | — |
| description Muted helper text under the label, linked with aria-describedby. | React.ReactNode | — |
| labelPosition Where the text sits. "left" makes a full-width settings row with the switch at the end. | "left" | "right" | "right" |
| loading Shows a spinner in the thumb and blocks toggling, e.g. while saving. | boolean | — |
| value Value submitted with name when checked; defaults to "on". | string | — |
| sound Plays "toggle-on" / "toggle-off" when the visitor has sounds on (lib/goodui-sounds); pass a sound name to use one sound for both. Off by default. | boolean | SoundName | — |
Also accepts every <button> attribute.
Accessibility
| Space / Enter | Toggles the switch (native <button>); does nothing while loading or disabled. |
| Tab | Moves focus to the switch; a focus ring shows on keyboard focus only. |
- A native <button role="switch"> with aria-checked, so screen readers announce on and off.
- label and description are linked with aria-labelledby and aria-describedby, and the whole row is a <label>, so clicking the text toggles it.
- Without label, pass aria-label. The thumb, its glyphs and the ON / OFF text are aria-hidden.
- loading sets aria-busy and ignores clicks; disabled disables the button and dims the row.
- icon-thumb and label-inside show the state with a glyph or text, not only with color.
Motion
- spring (default): the thumb slides on a spring (stiffness 620, damping 36). smooth: a 200 ms ease. bounce: a looser spring (stiffness 520, damping 13) with a squash and stretch of the thumb over 340 ms. none: it jumps.
- Press: the thumb stretches toward the travel direction (material grows to 1.12; neo pushes the whole track into its shadow).
- material shrinks the thumb when off unless it shows a glyph. Glyphs swap with a rotate and scale (300 ms, slight overshoot) and a 200 ms fade. Track color changes in 220 ms.
- Reduced motion: the thumb jumps with no press stretch and no transitions; the loading spinner slows to 1.6 s per turn instead of stopping.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- The callback is onCheckedChange(checked: boolean), not onChange. Use checked for controlled, defaultChecked otherwise.
- Pass label (and description) as props instead of wrapping the switch in your own <label>; without label, set aria-label.
- For native forms pass name (and optionally value): a hidden checkbox submits "on" (or value) when checked.
- onText and offText (default "ON" / "OFF") are only used by variant="label-inside"; translate them there. There is no labels prop.
- className goes on the track when there is no label or description, and on the row otherwise. tone only colors the on state.
FAQ
How do I show a save in progress?
Set loading while your request runs. The thumb shows a spinner, aria-busy is set and clicks are ignored; set checked when the request succeeds.
Switch or checkbox?
A switch applies immediately, like a light switch. A checkbox is a choice that is submitted later with a form.
Related
Checkbox and RadioGroup on real inputs with default, filled, outline, circle, neo and whole-card variants, draw, pop and bounce animations, tones, sizes, shadows, indeterminate, invalid and descriptions.
Switch with a springy thumb that squashes while pressed, icons that spin inside the thumb, three sizes, label and description.
Light/dark toggle that reveals the new theme as a circle from the click point with the View Transitions API while the sun morphs into a moon.
iOS-style segmented control whose thumb springs to the chosen segment and squeezes while pressed, with sizes, icons, full-width mode, dividers that hide next to the thumb, and radio-group arrow keys.