# Shadcn Switch

> Free shadcn/ui switch (toggle) component for React — a working dark-mode theme toggle, settings list, React Hook Form, sizes and icon examples. Copy or install in one command.

Source: https://designrevision.com/components/switch

The **Shadcn Switch** is the control for instant on/off settings — the canonical <a href="https://ui.shadcn.com/docs/components/switch" rel="nofollow">shadcn/ui</a> switch built on <a href="https://www.radix-ui.com/primitives/docs/components/switch" rel="nofollow">Radix</a>, themed entirely with CSS variables. It pairs with a [Label](/components/label) for a complete setting, and the examples below cover what you actually build: a real theme toggle, settings lists, and validated forms.

## When to use — switch vs checkbox

Reach for a switch when toggling takes effect **immediately** — enabling a feature, flipping a setting, turning notifications on. It reads as a physical toggle, so users expect an instant result. For selecting items, or opting in as part of a form you **submit**, use a [checkbox](/components/checkbox). A quick test: if there's a Save button, a checkbox is usually clearer; if the change is live, use a switch.

## Settings lists

The most common real-world switch UI is a **settings panel** — a card of labelled toggles, each with a short description, separated by dividers. Bind every switch to its own [Label](/components/label) with `htmlFor`/`id`, keep the label and description on the left and the switch on the right, and you have the canonical notifications/preferences layout. The Settings list example is the drop-in version.

## Theme toggle

A switch makes a natural **light/dark toggle**. Track the theme in state and flip the `dark` class on the document root from `onCheckedChange` — `document.documentElement.classList.toggle("dark", isDark)` — flanked by sun and moon icons. Our Theme toggle example is fully working (it changes the preview's theme as you flip it); in a real app you'd persist the choice with `next-themes` or `localStorage`.

## React Hook Form

The switch is fully controllable, so it slots into <a href="https://react-hook-form.com" rel="nofollow">React Hook Form</a>: drive each switch's `checked` from the field value and update it with `setValue` (or `field.onChange`) from `onCheckedChange`. The React Hook Form example is a complete notification-preferences form backed by a `zod` schema.

## Accessibility

Give every switch an accessible name — a bound `Label`, or an `aria-label` when there's no visible text (as in the theme and icon examples). The switch is keyboard-operable with a visible focus ring, and `disabled` uses the native attribute so it leaves the tab order. For a locked control, keep it `disabled` and explain why with text linked via `aria-describedby`.

## Theming

The switch reads your design tokens (`--primary`, `--input`, `--ring`), so it follows your theme — including dark mode — with zero overrides. Any palette produced by a shadcn-compatible theme generator applies automatically.

## Installation

### switch

**Install:**

```bash
npx shadcn@latest add @designrevision/switch
```

**Dependencies:** @radix-ui/react-switch, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checked | boolean | — | Controlled on/off state. |
| defaultChecked | boolean | false | Initial state when uncontrolled. |
| onCheckedChange | (checked: boolean) => void | — | Fires when the user toggles the switch. |
| disabled | boolean | false | Prevents interaction and dims the control. |

**Usage & accessibility:** Extends the Radix Switch Root (@radix-ui/react-switch). Size it by overriding the track height/width and the thumb (e.g. h-6 w-10 [&>span]:size-5).

```tsx
// components/ui/switch.tsx
"use client"

import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"

import { cn } from "@/lib/utils"

function Switch({
  className,
  ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      className={cn(
        "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
        className
      )}
      {...props}
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className="bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
      />
    </SwitchPrimitive.Root>
  )
}

export { Switch }
```

## Examples

### With label

A controlled switch with a label and description, toggled with `onCheckedChange` — the canonical setting field.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-label-01
```

**Dependencies:** switch, label

```tsx
// components/ui/switch-label-01.tsx
"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

export default function SwitchLabel01() {
  const [checked, setChecked] = React.useState(true)

  return (
    <div className="flex items-start gap-3">
      <Switch id="airplane" checked={checked} onCheckedChange={setChecked} />
      <div className="grid gap-1.5">
        <Label htmlFor="airplane">Airplane mode</Label>
        <p className="text-sm text-muted-foreground">
          Disable all wireless connections.
        </p>
      </div>
    </div>
  )
}
```

---

### Settings list

A card of labelled toggles with descriptions — the pattern behind every notification and preferences panel.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-settings-01
```

**Dependencies:** switch, label

```tsx
// components/ui/switch-settings-01.tsx
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

const settings = [
  {
    id: "marketing",
    label: "Marketing emails",
    description: "Receive emails about new products, features, and more.",
    defaultChecked: true,
  },
  {
    id: "security",
    label: "Security emails",
    description: "Receive emails about your account activity and security.",
    defaultChecked: true,
  },
  {
    id: "social",
    label: "Social notifications",
    description: "Get notified when someone follows or mentions you.",
    defaultChecked: false,
  },
]

export default function SwitchSettings01() {
  return (
    <div className="w-full max-w-md divide-y divide-border rounded-lg border border-border">
      {settings.map((item) => (
        <div
          key={item.id}
          className="flex items-center justify-between gap-4 p-4"
        >
          <div className="grid gap-1">
            <Label htmlFor={item.id}>{item.label}</Label>
            <p className="text-sm text-muted-foreground">{item.description}</p>
          </div>
          <Switch id={item.id} defaultChecked={item.defaultChecked} />
        </div>
      ))}
    </div>
  )
}
```

---

### Theme toggle

A **working** light/dark toggle flanked by sun and moon icons — it flips the `.dark` class, so the preview actually changes theme. No `next-themes` required.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-theme-01
```

**Dependencies:** lucide-react, switch, label

```tsx
// components/ui/switch-theme-01.tsx
"use client"

import * as React from "react"
import { Moon, Sun } from "lucide-react"

import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

export default function SwitchTheme01() {
  const [dark, setDark] = React.useState(false)

  React.useEffect(() => {
    document.documentElement.classList.toggle("dark", dark)
  }, [dark])

  return (
    <div className="flex items-center gap-3">
      <Sun className="size-4 text-muted-foreground" />
      <Switch
        id="theme"
        checked={dark}
        onCheckedChange={setDark}
        aria-label="Toggle dark mode"
      />
      <Moon className="size-4 text-muted-foreground" />
      <Label htmlFor="theme" className="sr-only">
        Dark mode
      </Label>
    </div>
  )
}
```

---

### React Hook Form

A notification-preferences form wired with `react-hook-form` and `zod` — drive `checked` from the field value and update it from `onCheckedChange`.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-form-01
```

**Dependencies:** react-hook-form, zod, @hookform/resolvers, switch, label, button

```tsx
// components/ui/switch-form-01.tsx
"use client"

import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

const schema = z.object({
  marketing: z.boolean(),
  security: z.boolean(),
})

type FormValues = z.infer<typeof schema>

const fields = [
  {
    name: "marketing",
    label: "Marketing emails",
    description: "Receive emails about new products and features.",
  },
  {
    name: "security",
    label: "Security emails",
    description: "Receive emails about your account security.",
  },
] as const

export default function SwitchForm01() {
  const [saved, setSaved] = React.useState(false)
  const { handleSubmit, setValue, watch } = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { marketing: true, security: true },
  })

  const values = watch()

  return (
    <form
      onSubmit={handleSubmit(() => setSaved(true))}
      className="grid w-full max-w-md gap-4"
    >
      <div className="divide-y divide-border rounded-lg border border-border">
        {fields.map((field) => (
          <div
            key={field.name}
            className="flex items-center justify-between gap-4 p-4"
          >
            <div className="grid gap-1">
              <Label htmlFor={field.name}>{field.label}</Label>
              <p className="text-sm text-muted-foreground">
                {field.description}
              </p>
            </div>
            <Switch
              id={field.name}
              checked={values[field.name]}
              onCheckedChange={(value) =>
                setValue(field.name, value, { shouldDirty: true })
              }
            />
          </div>
        ))}
      </div>
      <Button type="submit" className="w-fit">
        {saved ? "Saved!" : "Save preferences"}
      </Button>
    </form>
  )
}
```

---

### States

On, disabled, disabled-and-on, and a locked “upgrade to enable” switch with an explanatory hint.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-states-01
```

**Dependencies:** lucide-react, switch, label

```tsx
// components/ui/switch-states-01.tsx
import { Lock } from "lucide-react"

import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

export default function SwitchStates01() {
  return (
    <div className="grid w-full max-w-sm gap-5">
      <div className="flex items-center justify-between gap-4">
        <Label htmlFor="state-on">On by default</Label>
        <Switch id="state-on" defaultChecked />
      </div>
      <div className="flex items-center justify-between gap-4">
        <Label htmlFor="state-disabled" className="text-muted-foreground">
          Disabled
        </Label>
        <Switch id="state-disabled" disabled />
      </div>
      <div className="flex items-center justify-between gap-4">
        <Label htmlFor="state-disabled-on" className="text-muted-foreground">
          Disabled &amp; on
        </Label>
        <Switch id="state-disabled-on" defaultChecked disabled />
      </div>
      <div className="grid gap-1.5">
        <div className="flex items-center justify-between gap-4">
          <div className="flex items-center gap-1.5">
            <Lock className="size-3.5 text-muted-foreground" />
            <Label htmlFor="state-locked" className="text-muted-foreground">
              Advanced analytics
            </Label>
          </div>
          <Switch id="state-locked" disabled aria-describedby="locked-hint" />
        </div>
        <p id="locked-hint" className="text-xs text-muted-foreground">
          Upgrade to Pro to enable advanced analytics.
        </p>
      </div>
    </div>
  )
}
```

---

### Sizes

Small, default, and large — scale the track and thumb together with `h-* w-* [&>span]:size-*`.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-sizes-01
```

**Dependencies:** switch, label

```tsx
// components/ui/switch-sizes-01.tsx
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

export default function SwitchSizes01() {
  return (
    <div className="flex items-center gap-8">
      <div className="flex items-center gap-2">
        <Switch id="size-sm" defaultChecked className="h-4 w-6 [&>span]:size-3" />
        <Label htmlFor="size-sm" className="text-xs">
          Small
        </Label>
      </div>
      <div className="flex items-center gap-2">
        <Switch id="size-default" defaultChecked />
        <Label htmlFor="size-default">Default</Label>
      </div>
      <div className="flex items-center gap-2">
        <Switch
          id="size-lg"
          defaultChecked
          className="h-6 w-10 [&>span]:size-5"
        />
        <Label htmlFor="size-lg" className="text-base">
          Large
        </Label>
      </div>
    </div>
  )
}
```

---

### With icons

Check and X icons inside the track that fade in as the thumb slides — a pure-CSS on/off affordance.

**Install:**

```bash
npx shadcn@latest add @designrevision/switch-icon-01
```

**Dependencies:** lucide-react, switch

```tsx
// components/ui/switch-icon-01.tsx
"use client"

import * as React from "react"
import { Check, X } from "lucide-react"

import { Switch } from "@/components/ui/switch"

export default function SwitchIcon01() {
  const [on, setOn] = React.useState(true)

  return (
    <div className="relative inline-flex items-center">
      <Switch
        checked={on}
        onCheckedChange={setOn}
        aria-label="Toggle"
        className="h-6 w-11 [&>span]:size-5"
      />
      <Check
        className={`pointer-events-none absolute left-1.5 size-3 text-primary-foreground transition-opacity ${
          on ? "opacity-100" : "opacity-0"
        }`}
      />
      <X
        className={`pointer-events-none absolute right-1.5 size-3 text-muted-foreground transition-opacity ${
          on ? "opacity-0" : "opacity-100"
        }`}
      />
    </div>
  )
}
```
