Switch
A control that indicates whether a setting is on or off. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Label } from "@/registry/default/ui//label";
import { Switch } from "@/registry/default/ui//switch";
export default function Particle() {
return (
<Label>
<Switch />
Marketing emails
</Label>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/switch
Usage
import { Switch } from "@/components/ui/switch"<Switch />Examples
For accessible labelling and validation, prefer using the Field component to wrap checkboxes. See the related example: Switch field.
With Description
Add descriptive text below the label to clarify what enabling the switch will do.
By enabling marketing emails, you agree to receive emails.
import { useId } from "react";
import { Label } from "@/registry/default/ui//label";
import { Switch } from "@/registry/default/ui//switch";
export default function Particle() {
const id = useId();
return (
<div className="flex items-start gap-2">
<Switch defaultChecked id={id} />
<div className="flex flex-col gap-1">
<Label htmlFor={id}>Marketing emails</Label>
<p className="text-muted-foreground text-xs">
By enabling marketing emails, you agree to receive emails.
</p>
</div>
</div>
);
}
Sizes
Render sm, default, and lg size variants side by side to show the available scale options.
import { Label } from "@/registry/default/ui//label";
import { Switch } from "@/registry/default/ui//switch";
export function Pattern() {
return (
<div className="flex flex-col items-start justify-start gap-3">
<div className="flex items-center gap-2">
<Switch id="switch-sm" size="sm" />
<Label htmlFor="switch-sm">Small Switch</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="switch-default" size="default" />
<Label htmlFor="switch-default">Default Switch</Label>
</div>
</div>
);
}
Group
Group multiple switches together under a shared heading for related on/off settings.
import { Field, FieldLabel } from "@/registry/default/ui//field";
import { Switch } from "@/registry/default/ui//switch";
export function Pattern() {
return (
<Field className="w-auto space-y-2">
<FieldLabel>Notification Settings</FieldLabel>
<Field className="flex-row">
<Switch defaultChecked id="sg-email" />
<FieldLabel htmlFor="sg-email">Email notifications</FieldLabel>
</Field>
<Field className="flex-row">
<Switch id="sg-sms" />
<FieldLabel htmlFor="sg-sms">SMS notifications</FieldLabel>
</Field>
<Field className="flex-row">
<Switch defaultChecked id="sg-push" />
<FieldLabel htmlFor="sg-push">Push notifications</FieldLabel>
</Field>
</Field>
);
}
In Card With Icons
Each setting row pairs an icon, a label, and a switch inside a card — a common pattern for notification or feature-flag preferences.
import { Field, FieldLabel } from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";
export function Pattern() {
return (
<div className="flex flex-col gap-4">
<Field className="w-auto flex-row">
<Switch
className="data-checked:bg-blue-500"
defaultChecked
id="sw-blue"
/>
<FieldLabel htmlFor="sw-blue">Blue</FieldLabel>
</Field>
<Field className="w-auto flex-row">
<Switch
className="data-checked:bg-green-500"
defaultChecked
id="sw-green"
/>
<FieldLabel htmlFor="sw-green">Green</FieldLabel>
</Field>
<Field className="w-auto flex-row">
<Switch
className="data-checked:bg-yellow-500"
defaultChecked
id="sw-yellow"
/>
<FieldLabel htmlFor="sw-yellow">Yellow</FieldLabel>
</Field>
</div>
);
}
Destructive Confirmation
A high-impact toggle that shows confirmation text below the switch before the action is committed, preventing accidental activation.
When enabled, all local data will be permanently removed when you sign out. This action cannot be undone.
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";
export function Pattern() {
return (
<div className="mx-auto w-full max-w-xs">
<Field className="flex-row">
<div className="space-y-2">
<FieldLabel className="text-destructive" htmlFor="sw-danger ">
Delete all data on sign out
</FieldLabel>
<FieldDescription>
When enabled, all local data will be permanently removed when you
sign out. This action cannot be undone.
</FieldDescription>
</div>
<Switch className="data-checked:bg-destructive" id="sw-danger" />
</Field>
</div>
);
}
Settings Table
Displays multiple on/off settings as a scannable table with alternating rows, suitable for dense settings panels.
Editor Preferences
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
const settings = [
{
checked: true,
description: "Save changes automatically",
id: "auto-save",
label: "Auto-save",
},
{
checked: true,
description: "Highlight spelling errors",
id: "spell-check",
label: "Spell check",
},
{
checked: false,
description: "Show line numbers in editor",
id: "line-numbers",
label: "Line numbers",
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-md">
<p className="mb-3 font-medium text-sm">Editor Preferences</p>
<Separator />
<div className="flex flex-col">
{settings.map((setting) => (
<label
className="flex cursor-pointer items-center justify-between border-b py-3 last:border-b-0"
htmlFor={setting.id}
key={setting.id}
>
<div className="flex flex-col gap-0.5">
<span className="font-medium text-sm">{setting.label}</span>
<span className="text-muted-foreground text-xs">
{setting.description}
</span>
</div>
<Switch
defaultChecked={setting.checked}
id={setting.id}
size="sm"
/>
</label>
))}
</div>
</div>
);
}
Card Grid
A two-column card grid where each card contains a switch with a title and a short description — ideal for feature or plan settings.
"use client";
import { BarChart3Icon, BugIcon, DatabaseIcon, GlobeIcon } from "lucide-react";
import { useState } from "react";
import { Field, FieldLabel } from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";
const features = [
{
checked: true,
description: "Track page views and user interactions",
icon: <BarChart3Icon aria-hidden="true" className="size-4" />,
id: "feat-analytics",
title: "Analytics",
},
{
checked: true,
description: "Capture and report runtime errors",
icon: <BugIcon aria-hidden="true" className="size-4" />,
id: "feat-logging",
title: "Error Logging",
},
{
checked: false,
description: "Serve static assets from edge network",
icon: <GlobeIcon aria-hidden="true" className="size-4" />,
id: "feat-cdn",
title: "CDN Caching",
},
{
checked: false,
description: "Daily snapshots of your database",
icon: <DatabaseIcon aria-hidden="true" className="size-4" />,
id: "feat-backup",
title: "Auto Backup",
},
];
export function Pattern() {
const [checked, setChecked] = useState<Record<string, boolean>>(
Object.fromEntries(features.map((f) => [f.id, f.checked])),
);
return (
<div className="grid w-full max-w-lg grid-cols-2 gap-4">
{features.map((feature) => (
<Field key={feature.id}>
<FieldLabel
className={`h-full w-full cursor-pointer rounded-xl border p-3 transition-colors ${
checked[feature.id]
? "border-primary/5 bg-sidebar"
: "border-border"
}`}
htmlFor={feature.id}
>
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2">
<div
className={`flex shrink-0 items-center justify-center rounded-md border p-1.5 shadow-black/5 shadow-xs transition-colors ${
checked[feature.id]
? "border-primary/30 bg-primary/10 text-primary"
: "border-border bg-background"
}`}
>
{feature.icon}
</div>
<div className="flex flex-col items-start gap-0.5">
<span className="font-semibold text-sm">{feature.title}</span>
<span className="text-muted-foreground text-xs">
{feature.description}
</span>
</div>
</div>
<Switch
checked={checked[feature.id]}
id={feature.id}
onCheckedChange={(val) =>
setChecked((prev) => ({ ...prev, [feature.id]: val }))
}
/>
</div>
</FieldLabel>
</Field>
))}
</div>
);
}
Cookie Preferences
A cookie consent manager grouping switches into Always On, Analytics, and Marketing categories, each with a contextual badge that reflects the current state.
Your preferences are saved locally and can be updated at any time.
"use client";
import { ShieldCheckIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardPanel,
CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
const cookieCategories = [
{
defaultChecked: true,
description: "Required for the website to function. Cannot be disabled.",
disabled: true,
id: "necessary",
title: "Strictly Necessary",
},
{
defaultChecked: true,
description: "Help us understand how visitors interact with our site.",
disabled: false,
id: "analytics",
title: "Analytics & Performance",
},
{
defaultChecked: false,
description: "Used to deliver personalised ads relevant to you.",
disabled: false,
id: "marketing",
title: "Marketing & Targeting",
},
{
defaultChecked: false,
description: "Remember your preferences and customise your experience.",
disabled: false,
id: "personalization",
title: "Personalization",
},
];
export function Pattern() {
const [consent, setConsent] = useState<Record<string, boolean>>(
Object.fromEntries(cookieCategories.map((c) => [c.id, c.defaultChecked])),
);
const activeCount = Object.values(consent).filter(Boolean).length;
return (
<Card className="w-full max-w-md">
<CardHeader className="border-b">
<div className="flex items-center gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<ShieldCheckIcon className="size-4" />
</div>
<div>
<CardTitle>Cookie Preferences</CardTitle>
<CardDescription>
{activeCount} of {cookieCategories.length} categories enabled
</CardDescription>
</div>
</div>
</CardHeader>
<CardPanel className="p-0">
{cookieCategories.map((category, index) => (
<div key={category.id}>
<label
className="flex cursor-pointer items-start justify-between gap-4 px-6 py-4"
htmlFor={category.id}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{category.title}</span>
{category.disabled ? (
<Badge size="sm" variant="success">
Always On
</Badge>
) : consent[category.id] ? (
<Badge size="sm" variant="info">
Active
</Badge>
) : (
<Badge size="sm" variant="outline">
Off
</Badge>
)}
</div>
<span className="text-muted-foreground text-xs">
{category.description}
</span>
</div>
<Switch
checked={consent[category.id]}
disabled={category.disabled}
id={category.id}
onCheckedChange={(val) =>
setConsent((prev) => ({ ...prev, [category.id]: val }))
}
size="sm"
/>
</label>
{index < cookieCategories.length - 1 && <Separator />}
</div>
))}
</CardPanel>
<CardFooter className="border-t bg-muted/30">
<p className="text-muted-foreground text-xs">
Your preferences are saved locally and can be updated at any time.
</p>
</CardFooter>
</Card>
);
}
Notification Delivery Channels
A master toggle that enables or disables all delivery channels at once. Individual channel rows (email, SMS, push, Slack) each carry their own switch and are dimmed while the master is off.
Delivery channels
"use client";
import {
BellIcon,
BellOffIcon,
MailIcon,
MessageSquareIcon,
MonitorIcon,
SmartphoneIcon,
} from "lucide-react";
import { useState } from "react";
import {
Card,
CardDescription,
CardHeader,
CardPanel,
CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
const channels = [
{
description: "Receive updates directly to your inbox",
icon: MailIcon,
id: "channel-email",
label: "Email",
},
{
description: "Alerts sent to your registered phone",
icon: SmartphoneIcon,
id: "channel-sms",
label: "SMS",
},
{
description: "Banner alerts while using the app",
icon: MonitorIcon,
id: "channel-in-app",
label: "In-app",
},
{
description: "Notifications pushed to your device",
icon: MessageSquareIcon,
id: "channel-push",
label: "Push",
},
];
export function Pattern() {
const [globalEnabled, setGlobalEnabled] = useState(true);
const [channelState, setChannelState] = useState<Record<string, boolean>>({
"channel-email": true,
"channel-in-app": true,
"channel-push": false,
"channel-sms": false,
});
const toggle = (id: string, val: boolean) =>
setChannelState((prev) => ({ ...prev, [id]: val }));
return (
<Card className="w-full max-w-sm">
<CardHeader className="border-b">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div
className={`flex size-9 shrink-0 items-center justify-center rounded-lg transition-colors ${
globalEnabled
? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground"
}`}
>
{globalEnabled ? (
<BellIcon className="size-4" />
) : (
<BellOffIcon className="size-4" />
)}
</div>
<div>
<CardTitle>Notifications</CardTitle>
<CardDescription>
{globalEnabled ? "Receiving alerts" : "All alerts paused"}
</CardDescription>
</div>
</div>
<Switch
checked={globalEnabled}
id="global-notifications"
onCheckedChange={setGlobalEnabled}
/>
</div>
</CardHeader>
<CardPanel className="p-0">
<div
className={`transition-opacity ${globalEnabled ? "opacity-100" : "pointer-events-none opacity-40"}`}
>
<p className="px-5 pt-4 pb-2 font-medium text-muted-foreground text-xs uppercase tracking-wider">
Delivery channels
</p>
{channels.map((channel, index) => {
const Icon = channel.icon;
const isActive = globalEnabled && channelState[channel.id];
return (
<div key={channel.id}>
<label
className="flex cursor-pointer items-center gap-3 px-5 py-3"
htmlFor={channel.id}
>
<div
className={`flex size-8 shrink-0 items-center justify-center rounded-md border transition-colors ${
isActive
? "border-primary/20 bg-primary/8 text-primary"
: "border-border bg-muted/50 text-muted-foreground"
}`}
>
<Icon className="size-3.5" />
</div>
<div className="min-w-0 flex-1">
<p className="font-medium text-sm">{channel.label}</p>
<p className="truncate text-muted-foreground text-xs">
{channel.description}
</p>
</div>
<Switch
checked={channelState[channel.id]}
id={channel.id}
onCheckedChange={(val) => toggle(channel.id, val)}
size="sm"
/>
</label>
{index < channels.length - 1 && <Separator className="ml-16" />}
</div>
);
})}
</div>
</CardPanel>
</Card>
);
}
Plan Feature Toggles
A feature list where pro features are togglable and enterprise-only features are shown with a badge and rendered as disabled.
"use client";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
const features = [
{ id: "analytics", label: "Advanced Analytics", pro: true },
{ id: "api", label: "API Access", pro: true },
{ id: "export", label: "Data Export", pro: true },
{ id: "sso", label: "Single Sign-On", pro: true },
{ id: "audit", label: "Audit Logs", pro: false },
{ id: "support", label: "Priority Support", pro: false },
];
export default function Particle() {
const [enabled, setEnabled] = useState<Record<string, boolean>>({
analytics: true,
api: false,
audit: false,
export: true,
sso: false,
support: false,
});
return (
<div className="w-full max-w-sm space-y-1 rounded-xl border p-1">
{features.map(({ id, label, pro }) => (
<div
className="flex items-center justify-between rounded-lg px-3 py-2.5 hover:bg-muted/50"
key={id}
>
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{label}</span>
{!pro && (
<Badge className="text-[10px]" variant="secondary">
Enterprise
</Badge>
)}
</div>
<Switch
checked={enabled[id]}
disabled={!pro}
onCheckedChange={(v) =>
setEnabled((prev) => ({ ...prev, [id]: v }))
}
/>
</div>
))}
</div>
);
}
Privacy Settings
A permission panel with an icon, label, and description for each setting. A separator divides the media permissions from the tracking and ad preferences.
Privacy Settings
Location Services
Allow apps to access your location
Camera & Microphone
Grant access to media devices
Microphone Only
Used for voice features
Activity Tracking
Help improve your experience
Personalised Ads
See relevant advertisements
"use client";
import {
EyeIcon,
MapPinIcon,
MicIcon,
ShieldIcon,
SmartphoneIcon,
} from "lucide-react";
import { useState } from "react";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
const settings = [
{
description: "Allow apps to access your location",
Icon: MapPinIcon,
id: "location",
label: "Location Services",
},
{
description: "Grant access to media devices",
Icon: SmartphoneIcon,
id: "camera",
label: "Camera & Microphone",
},
{
description: "Used for voice features",
Icon: MicIcon,
id: "microphone",
label: "Microphone Only",
},
{
description: "Help improve your experience",
Icon: EyeIcon,
id: "activity",
label: "Activity Tracking",
},
{
description: "See relevant advertisements",
Icon: ShieldIcon,
id: "ads",
label: "Personalised Ads",
},
];
export default function Particle() {
const [values, setValues] = useState<Record<string, boolean>>({
activity: true,
ads: false,
camera: false,
location: true,
microphone: false,
});
return (
<div className="w-full max-w-sm overflow-hidden rounded-xl border">
<div className="border-b px-4 py-3">
<p className="font-semibold text-sm">Privacy Settings</p>
</div>
<div className="divide-y">
{settings.map(({ id, Icon, label, description }, i) => (
<div key={id}>
<div className="flex items-center gap-3 px-4 py-3">
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted">
<Icon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
</div>
<div className="flex-1">
<p className="font-medium text-sm">{label}</p>
<p className="text-muted-foreground text-xs">{description}</p>
</div>
<Switch
checked={values[id]}
onCheckedChange={(v) =>
setValues((prev) => ({ ...prev, [id]: v }))
}
size="sm"
/>
</div>
{i === 2 && <Separator />}
</div>
))}
</div>
</div>
);
}
Dark Mode Toggle
An animated card that transitions between light and dark appearances. Sun and Moon icons flank the switch to reinforce the current mode.
Light Mode
Crisp and clear in daylight
"use client";
import { MoonIcon, SunIcon } from "lucide-react";
import { useState } from "react";
import { Switch } from "@/components/ui/switch";
export default function Particle() {
const [dark, setDark] = useState(false);
return (
<div
className={`flex w-full max-w-xs flex-col items-center gap-6 rounded-2xl border p-8 transition-colors duration-300 ${dark ? "border-zinc-700 bg-zinc-900 text-white" : "bg-white text-zinc-900"}`}
>
<div
className={`flex size-16 items-center justify-center rounded-full transition-colors duration-300 ${dark ? "bg-zinc-800" : "bg-amber-50"}`}
>
{dark ? (
<MoonIcon aria-hidden="true" className="size-7 text-indigo-400" />
) : (
<SunIcon aria-hidden="true" className="size-7 text-amber-500" />
)}
</div>
<div className="text-center">
<p className="font-semibold">{dark ? "Dark Mode" : "Light Mode"}</p>
<p
className={`text-sm ${dark ? "text-zinc-400" : "text-zinc-500"} mt-1`}
>
{dark ? "Easy on the eyes at night" : "Crisp and clear in daylight"}
</p>
</div>
<div className="flex items-center gap-3">
<SunIcon
aria-hidden="true"
className={`size-4 ${dark ? "text-zinc-500" : "text-amber-500"}`}
/>
<Switch
checked={dark}
className="data-checked:bg-indigo-600"
onCheckedChange={setDark}
/>
<MoonIcon
aria-hidden="true"
className={`size-4 ${dark ? "text-indigo-400" : "text-zinc-400"}`}
/>
</div>
</div>
);
}
Required Agreement
A sign-up form with a required Terms & Conditions switch. The submit button remains disabled until the user has toggled the switch on.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
export default function Particle() {
const [agreed, setAgreed] = useState(false);
return (
<form
className="w-full max-w-sm space-y-4 rounded-xl border p-5"
onSubmit={(e) => e.preventDefault()}
>
<p className="font-semibold">Create Account</p>
<Field>
<FieldLabel>Full name</FieldLabel>
<Input placeholder="Jane Smith" type="text" />
</Field>
<Field>
<FieldLabel>Email</FieldLabel>
<Input placeholder="jane@example.com" type="email" />
</Field>
<Field className="flex-row items-start gap-3">
<Switch
checked={agreed}
className="mt-0.5"
onCheckedChange={setAgreed}
required
/>
<div>
<FieldLabel>Terms & Conditions</FieldLabel>
<FieldDescription>
I agree to the Terms of Service and Privacy Policy
</FieldDescription>
</div>
</Field>
<Button className="w-full" disabled={!agreed} type="submit">
Create Account
</Button>
</form>
);
}
Do Not Disturb
A do-not-disturb toggle that expands a duration picker when enabled. The icon and description text update to reflect whether the mode is active.
Do Not Disturb
All notifications enabled
"use client";
import { BellOffIcon, MoonIcon } from "lucide-react";
import { useState } from "react";
import { Switch } from "@/components/ui/switch";
const scheduleOptions = ["1 hour", "2 hours", "4 hours", "Until tomorrow"];
export default function Particle() {
const [dnd, setDnd] = useState(false);
const [selected, setSelected] = useState("1 hour");
return (
<div className="w-full max-w-sm space-y-4 rounded-xl border p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className={`flex size-9 items-center justify-center rounded-lg transition-colors ${dnd ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"}`}
>
{dnd ? (
<BellOffIcon aria-hidden="true" className="size-4" />
) : (
<MoonIcon aria-hidden="true" className="size-4" />
)}
</div>
<div>
<p className="font-medium text-sm">Do Not Disturb</p>
<p className="text-muted-foreground text-xs">
{dnd ? `Active for ${selected}` : "All notifications enabled"}
</p>
</div>
</div>
<Switch checked={dnd} onCheckedChange={setDnd} />
</div>
{dnd && (
<div className="space-y-2">
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Duration
</p>
<div className="grid grid-cols-2 gap-2">
{scheduleOptions.map((opt) => (
<button
className={`rounded-lg border px-3 py-2 font-medium text-xs transition-colors ${
selected === opt
? "border-primary bg-primary/5 text-primary"
: "hover:bg-muted"
}`}
key={opt}
onClick={() => setSelected(opt)}
type="button"
>
{opt}
</button>
))}
</div>
</div>
)}
</div>
);
}

