Spring switch
Free · MITSwitch with a springy thumb that squashes while pressed, icons that spin inside the thumb, three sizes, label and description.
Installation
$ npx shadcn@latest add https://goodui.dev/r/switch-spring.jsonThe file lands in components/goodui/switch-spring.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/switch-spring works too.
Usage
import * as React from "react"
import { SwitchSpring } from "@/components/goodui/switch-spring"
export function ReminderSettings() {
const [sms, setSms] = React.useState(true)
return (
<div className="rounded-xl border bg-card p-4">
<SwitchSpring
checked={sms}
onCheckedChange={setSms}
label="SMS reminders"
description={sms ? "Clients get a text 24 hours before each visit." : "Clients won't be reminded."}
labelPosition="start"
/>
</div>
)
}When to use
- Settings that take effect immediately: SMS reminders, auto-confirm bookings, email digests.
- Settings rows that need a label and a helper line that can change with the state.
- Mode toggles where an icon inside the thumb explains the state (sun/moon, check/x).
- A choice that is only applied when a form is submitted, or a list of items to tick: use checkbox-draw.
- Switching the whole site theme with a single icon button: use theme-toggle.
- Choosing between more than two options: use segmented-control.
| If you need | Use |
|---|---|
| Dense settings lists and tables | size="sm" |
| Standard settings rows | size="md" (default) with label and description |
| A prominent mode toggle | size="lg" with icons={{ on: <Moon />, off: <Sun /> }} |
| Label on the left, switch on the right (settings pages) | labelPosition="start" |
Examples
Icons inside the thumb
import { Moon, Sun } from "lucide-react"
<SwitchSpring
size="lg"
defaultChecked={false}
icons={{ on: <Moon />, off: <Sun /> }}
label="Quiet hours"
description="Pause notifications from 9 pm to 7 am."
/>In a form, without a visible label
<form action={saveBillingSettings}>
<SwitchSpring name="autoRenew" defaultChecked aria-label="Auto-renew subscription" />
<button type="submit">Save</button>
</form>
// Submits autoRenew=on or autoRenew=offAPI reference
| Prop | Type | Default |
|---|---|---|
| name Form field name; a hidden input submits "on" when checked and "off" when not. | string | — |
| checked Controlled on/off state; pair with onCheckedChange. | boolean | — |
| defaultChecked Initial state when uncontrolled. | boolean | false |
| onCheckedChange Called with the new state on every toggle (click, Space or Enter). | (checked: boolean) => void | — |
| size "sm" 20×36px, "md" 24×44px, "lg" 32×56px track. | "sm" | "md" | "lg" | "md" |
| icons Icons drawn inside the thumb, e.g. { on: <Moon />, off: <Sun /> }. | { on: React.ReactNode; off: React.ReactNode } | — |
| label Clickable label next to the switch; also its accessible name. | React.ReactNode | — |
| description Muted helper text under the label, linked with aria-describedby. | React.ReactNode | — |
| labelPosition Put the label before the switch instead of after it. | "start" | "end" | "end" |
Also accepts every <button> attribute.
Accessibility
| Space / Enter | Toggles the switch (native <button>); the thumb squashes while the key is held. |
| Tab | Moves focus to the switch; a focus ring shows on keyboard focus only. |
- Renders a native <button role="switch"> with aria-checked, so screen readers announce it as on or off.
- label renders a <label htmlFor> tied to the button, so clicking the text toggles it and it names the switch; description is linked with aria-describedby.
- Without label, pass aria-label so the switch has a name.
- The thumb and its icons are aria-hidden; the state comes from aria-checked only.
Motion
- The thumb slides on a spring (stiffness 520, damping 24, mass 0.7).
- Pressing with the pointer or holding Space/Enter stretches the thumb to scaleX 1.22 and scaleY 0.9 on a stiffer spring, anchored to the side it rests on.
- Icons swap with a spin and scale: the old one turns away while the new one rotates in from ±120 degrees.
- The track color fades over 200 ms (CSS).
- Reduced motion: the thumb jumps without a spring, the squash is off, and icons cross-fade in 100 ms.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- Use checked + onCheckedChange (not value/onChange); onCheckedChange receives a boolean.
- className goes on the <button> track, not the wrapper; recolor the on state with a class like data-[state=checked]:bg-emerald-500.
- icons needs both on and off; pass lucide icons without size classes, the thumb sizes them.
- With name, the hidden input submits "on" or "off" (unlike a native checkbox, which omits unchecked values). Parse both on the server.
- Only sm, md and lg sizes exist; do not invent xs or xl.
FAQ
How do I change the on color?
Pass a class for the checked state on className, e.g. data-[state=checked]:bg-emerald-500. By default it uses your theme's --primary.
Does it submit with a form?
Yes, when you pass name. A hidden input sends "on" when checked and "off" when not.
Related
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.
Checkbox whose check stroke draws itself in over a popping fill, with an indeterminate dash and an animated strike-through for checklists.
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.