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.

Installation

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

@radix-ui/react-switch
utils

Props

checked = —
boolean

Controlled on/off state.

defaultChecked = false
boolean

Initial state when uncontrolled.

onCheckedChange = —
(checked: boolean) => void

Fires when the user toggles the switch.

disabled = false
boolean

Prevents interaction and dims the control.

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).

Copied!
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.

Packages

switch
label

Props

No props documented yet.

Copied!
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.

Packages

switch
label

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
switch
label

Props

No props documented yet.

Copied!
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`.

Packages

react-hook-form
zod
@hookform/resolvers
switch
label
button

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
switch
label

Props

No props documented yet.

Copied!
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 & 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-*`.

Packages

switch
label

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
switch

Props

No props documented yet.

Copied!
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>
  )
}

Frequently asked questions

Yes — it is the canonical shadcn/ui switch, a Radix Switch styled with your theme tokens, so it drops into any shadcn/ui project. On top of it we ship the patterns the original leaves out: a working theme toggle, a settings list, React Hook Form, sizes, and icons.
Use a switch for an instant on/off that takes effect immediately, like a setting — it reads as a physical toggle. Use a [checkbox](/components/checkbox) for selecting items or opting in as part of a form you submit. If toggling does not apply until you click Save, a checkbox is often the clearer choice.
Track the theme in state and toggle the dark class on the document root from onCheckedChange — document.documentElement.classList.toggle("dark", isDark). The Theme toggle example does exactly this; in a real app you would typically persist it with next-themes or localStorage.
The switch uses onCheckedChange, which receives a boolean — true for on, false for off. Store it in state for a controlled switch, or read defaultChecked for an uncontrolled one. There is no native onChange.
Override the track height and width and scale the thumb in the same className, for example h-6 w-10 [&>span]:size-5 for a large switch. The thumb travel is relative, so it follows the new size. The Sizes example shows small, default, and large.
Yes. It is styled with CSS variable tokens like --primary, --input, and --ring, so it follows your theme automatically, including dark mode and any palette from a shadcn-compatible theme generator.