Switch
A spring-driven React switch whose thumb can be dragged to toggle, with optional crossfading icons inside the thumb, a pressed-state toggle-button variant and native form submission.
The switch every settings page needs. Click it, press Space / Enter, or grab the thumb and drag it across; the thumb moves on a spring, the track colour follows the drag progress, and on release it snaps to the far side if you crossed the halfway point or springs back otherwise. You can put on/off icons inside the thumb, which rotate and fade into each other when toggling. variant="button" turns it into a pressed-state toggle button (aria-pressed), suited to "pressed = on" cases like a text toolbar. State supports controlled and uncontrolled modes, and passing name submits it with a native form.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add switch.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<Switch/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/switch.en.jsonOr, once registries are configured in components.json, install it as @webberui/switch.
Usage
import { Moon, Sun } from "lucide-react";
import { Switch } from "@/components/ui/switch";
const [dark, setDark] = React.useState(false);
<Switch
label="Dark mode"
description="Switch the interface colours to match your preference"
checked={dark}
onCheckedChange={setDark}
icons={{ on: <Moon />, off: <Sun /> }}
/>
// Pressed-state toggle button
<Switch variant="button" label="Bold" defaultChecked />
// Native form: submits newsletter=yes when on
<Switch name="newsletter" value="yes" label="Subscribe to the newsletter" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | — | Controlled checked state; when omitted the component manages it internally (uncontrolled) |
defaultChecked | boolean | false | Initial state in uncontrolled mode |
onCheckedChange | (checked: boolean) => void | — | Called when the state changes, with the new state |
size | "sm" | "md" | "lg" | "md" | Size: the track is 28×16, 36×20 or 44×24 |
disabled | boolean | false | Disabled: cannot be clicked, dragged or toggled from the keyboard, and is greyed out |
label | ReactNode | — | Label; rendered as a clickable label in the switch variant and as the button text in the button variant |
description | ReactNode | — | Supporting description, linked via aria-describedby |
variant | "switch" | "button" | "switch" | Visual variant: sliding switch or pressed-state toggle button |
icons | SwitchIcons | — | On/off icons inside the thumb, crossfaded when toggling |
stiffness | number | 500 | Thumb spring stiffness |
damping | number | 30 | Thumb spring damping |
name | string | — | Native form field name; when set, a hidden input is submitted with the form |
value | string | "on" | Value submitted with the form when on |
className | string | — | Passed through to the outermost container |
SwitchIcons
| Field | Type | Default | Description |
|---|---|---|---|
on | ReactNode | — | Icon shown when on |
off | ReactNode | — | Icon shown when off |
How it works
- Dragging: pressing on the thumb locks the pointer with
setPointerCapture; the thumb follows the pointer, clamped to the track. On release it snaps to the far side if it crossed the halfway point, otherwise it springs back. Movement under 3px counts as a click, and the click the browser fires after a drag is swallowed so the switch does not toggle twice. - The track's on-colour is a layer over the base colour: its opacity follows the drag progress 0–1, and otherwise transitions over
--wb-switch-duration(150ms by default). Override the variable globally or on a parent to change the pacing. - In dark mode the on-track is light, so the thumb turns dark to keep contrast.
- The switch is not a text input; Space / Enter are handled by the native
<button>, so IME composition never interferes. - Form submission mirrors a native checkbox:
name=valueis only sent when on, and nothing is sent when off.
Accessibility
- The switch variant is a
<button role="switch">witharia-checked;labelis linked viahtmlFor, so clicking the text toggles it too - The button variant is a
<button aria-pressed>(notrole="switch"), suited to toolbars; in this variantdescriptionis provided to screen readers as visually hidden text descriptionis linked viaaria-describedby; the thumb and icons are hidden from assistive technology- Keyboard: Tab to focus, then Space or Enter toggles; a focus-visible ring shows on focus; when
disabledit cannot be focused or operated - When the user has reduce motion enabled, the thumb jumps into place instead of springing, dragging is disabled (click only), icons only fade without rotating, and the toggle button no longer scales when held
Tactile Button
A two-layer physical key React component — the face floats above a solid base, dips 2px on hover and bottoms out on press, with depth from solid colour rather than blurred shadows.
Labeled Mode Switch
A two-state text switch (e.g. Ask | Auto) with a glassy spring-driven thumb and labels that cross-fade with the thumb position — for operating-mode toggles.