# Switch

> A React toggle switch for settings that apply at once: a spring thumb that stretches and drags, thumb icons, and async saves that roll back.

- Type: React component (Inputs)
- Access: Free (the source is open to everyone)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add switch`
- Docs: https://wingo-ui.com/components/switch

## Interaction

Click the track, its label or the empty space of the row and the thumb springs across while the track fills in the tone. Hold it down and the thumb stretches toward where it is going, then settles as it lands; drag the thumb sideways like on an iPhone and it follows your finger, committing past half way and snapping back otherwise. A vertical swipe on a phone still scrolls the page. Tab reaches the switch with a ring around the track; Space and Enter toggle it, and holding Space stretches the thumb until you let go. When saving is async the switch flips at once and a spinner turns in the thumb; clicks and keys are ignored until it settles, and a failed save flips it back, shakes the row and shows the error under it (press Simulate an error in the playground). Read-only switches keep their value and ignore input, disabled ones dim. Cards take a ring while on and dip slightly when pressed. On a phone every row is at least 44px tall and the whole row is the target; a switch without a label gets a 44px hit area. With reduced motion the thumb jumps, nothing stretches or shakes, and the spinner turns slower.

## Installation

```bash
npx wingo-ui@latest add switch
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/switch.json`

Files:

- `components/ui/switch.tsx`

npm dependencies: `npm i @radix-ui/react-switch motion`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))

## Usage

```tsx
import { useState } from "react"
import { Switch } from "@/components/ui/switch"

async function saveSetting(key: string, on: boolean) {
  const res = await fetch("/api/settings", { method: "PATCH", body: JSON.stringify({ [key]: on }) })
  if (!res.ok) throw new Error("save failed")
}

export function NotificationSettings() {
  const [messages, setMessages] = useState(true)

  return (
    <div className="flex flex-col divide-y divide-foreground/10 rounded-2xl bg-surface shadow-raised">
      <Switch
        label="New messages"
        description="When someone writes to you about a listing"
        labelPosition="start"
        checked={messages}
        onCheckedChange={setMessages}
        classNames={{ field: "min-h-14 px-4 py-3" }}
      />
      <Switch
        label="Price offers"
        labelPosition="start"
        defaultChecked
        errorText="Could not save. Try again."
        onCheckedChange={(on) => saveSetting("offers", on)}
        classNames={{ field: "min-h-14 px-4 py-3", error: "px-4 pb-3" }}
      />
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | Text beside the track. The whole row is one <label>, so clicking the text, the description or the empty space toggles it. It names the switch through aria-labelledby. Leave it empty for a bare switch (then pass aria-label). |
| `description?` | `ReactNode` | `""` | A second line under the label, in the muted caption style. It stays out of the switch's name and is linked with aria-describedby, so screen readers read it after the label and the state. |
| `error?` | `ReactNode` | `""` | A message under the row, lined up with the label, that slides in and marks the switch invalid (red ring, aria-invalid). "", false and null are no error; true only marks it invalid. |
| `required?` | `boolean` | `false` | A red mark after the label (hidden from screen readers) and required on the switch, so a native form will not submit while it is off. |
| `invalid?` | `boolean` | `false` | The red ring and aria-invalid without a message, for forms that show errors elsewhere. |
| `disabled?` | `boolean` | `false` | Dims the row to 50% and blocks every input. A disabled Field or FieldGroup around it disables it too. |
| `readOnly?` | `boolean` | `false` | Shows the value but ignores clicks, keys and drags, with no hover tint and aria-readonly. It stays focusable, so it is still read out. |
| `defaultChecked?` | `boolean` | `false` | The starting value while uncontrolled. Pass checked and onCheckedChange to control it. |
| `loading?` | `boolean` | `false` | A spinner turns inside the thumb and clicks, keys and drags are ignored, with aria-busy. For a save you track yourself; an async onCheckedChange does the same on its own. |
| `errorText?` | `ReactNode` | `"Could not save. Try again."` | Shown under the row when an async onCheckedChange rejects, and announced. It clears on the next change. Press Simulate an error in the playground to see it. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Track 32x18 / 40x24 / 48x28px with a 14 / 20 / 24px thumb, label 13 / 14 / 15px. Rows grow to 44px on touch screens; the track keeps its size. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The track color while on. neutral is black in light mode and white in dark mode, where the thumb turns dark so it still stands out. |
| `color?` | `string` | `""` | Any CSS color for the track while on. Replaces the tone; the thumb turns black or white by the color's lightness. Leave empty to use the tone. |
| `variant?` | `"default" \| "card"` | `"default"` | card turns the row into a soft-filled tile (the checkbox card look) with the switch at the top end; it takes a ring in the tone while on and dips slightly when pressed. Pair it with icon. |
| `icons?` | `boolean` | `false` | A check inside the thumb while on and an x while off, swapping with a crossfade and a small turn. They stay undistorted while the thumb stretches. |
| `labelPosition?` | `"end" \| "start"` | `"end"` | end: the switch first, the text after it. start: the text first and the switch at the end of a full-width row, like an iOS settings list. Cards always put the switch at the end. |
| `checked?` | `boolean` |  | The controlled value. With an async onCheckedChange the switch shows the value being saved until the promise settles, then follows checked again, so set checked after the save succeeds (or revert it yourself when it fails) for the rollback to show. |
| `onCheckedChange?` | `(checked: boolean) => (void \| Promise<unknown>)` |  | Called with the new value on every click, key or drag. Return a promise to save first: the switch flips at once, spins, ignores input until it settles, and a rejection flips it back, shakes the row and shows errorText. A late result after unmount is ignored. |
| `checkedIcon?` | `ReactNode` |  | A custom icon inside the thumb while on, like a Moon. Unsized svgs follow the size (10 / 12 / 14px) and take the tone color. |
| `uncheckedIcon?` | `ReactNode` |  | A custom icon inside the thumb while off, like a Sun. |
| `icon?` | `ReactNode` |  | Cards: the leading icon tile, which takes the tone's soft color while on. Rows: an icon, thumbnail or avatar before the text, centered on the row. |
| `name?` | `string` | `""` | Form field name. Inside a <form> Radix renders a hidden checkbox that submits value while the switch is on (nothing while off), and a form reset returns it to defaultChecked. The demo prints what the form would submit. |
| `value?` | `string` | `"on"` | What the form submits while the switch is on. |
| `id?` | `string` |  | The switch's id. Inside a Field it reuses the Field's, so the Field's label points at it. |
| `classNames?` | `Partial<Record<"field" \| "track" \| "thumb" \| "icon" \| "content" \| "label" \| "description" \| "error", string>>` |  | Extra classes for the parts: the row or card, the track, the thumb, the icon layer, the text column, the label, the description and the error. |
| `className?` | `string` |  | Classes for the outermost element: the wrapper around the row and its message, or the track itself when the switch has no text. Override the tone vars here, e.g. [--tone:var(--success)]. |
| `ref?` | `RefObject<HTMLButtonElement \| null> or (node) => void` |  | The track: the Radix switch <button role="switch">, which also receives every other prop. |

## Examples

- **Async save with rollback**: Return a promise: the switch flips at once and spins. Turn on the error and the save fails, it flips back and shakes.
- **Sizes and tones**: 32, 40 and 48px tracks; any tone or CSS color for the on state.
- **Thumb icons**: Your own icons (a moon and a sun) or the built-in check and x. They never distort while the thumb stretches.
- **Cards**: A bordered tile with an icon; the ring follows the value and the tile dips when pressed.
- **Label position, disabled and read-only**: end puts the switch first, start at the end of a full-width row. Read-only keeps the value and ignores input.
- **In a native form**: name and value submit through FormData while on, required blocks the submit, and reset puts every switch back.
- **Without a label, in a list**: A bare switch named with aria-label; on touch it still gets a 44px hit area.

## See also

- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Toggle](https://wingo-ui.com/components/toggle) ([Markdown](https://wingo-ui.com/components/toggle.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))

---

Canonical: https://wingo-ui.com/components/switch
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
