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.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
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).
"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
Props
No props documented yet.
"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
Props
No props documented yet.
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
Props
No props documented yet.
"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
Props
No props documented yet.
"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
Props
No props documented yet.
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
Props
No props documented yet.
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
Props
No props documented yet.
"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>
)
}
The Shadcn Switch is the control for instant on/off settings — the canonical shadcn/ui switch built on Radix, themed entirely with CSS variables. It pairs with a 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. 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 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 React Hook Form: 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.