Switch

Free · MIT

iOS, 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.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/switch.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Default app settingsvariant="ios" (default) with animation="spring"
Android-like or Material appsvariant="material" (the thumb grows when on)
State that must be readable without colorvariant="icon-thumb" (check / x) or "label-inside" (ON / OFF)
Theme switch in a settings pageicons="theme" (sun / moon in the thumb)
Retro or playful brandsvariant="neo" or "square"
Calm, no-bounce motionanimation="smooth" or "none"
Settings rowlabel, 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

PropTypeDefault
defaultChecked
Initial state when uncontrolled.
booleanfalse
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 / EnterToggles the switch (native <button>); does nothing while loading or disabled.
TabMoves 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