Switch
Turns a setting on or off, taking effect the moment it's flipped.
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function SwitchDemo() {
return (
<Field orientation="horizontal" className="w-60">
<Switch id="auto-deploy" defaultChecked />
<FieldLabel htmlFor="auto-deploy">Deploy on every push</FieldLabel>
</Field>
)
}Anatomy
- 1TrackBrand-filled when on, neutral when off. Its hit area extends invisibly to 44×36px.
- 2ThumbSlides across on a spring when toggled, and stretches while pressed.
- 3LabelNames the setting, not the action. Clicking it toggles the switch too.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/switch.jsonUsage
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"<Field orientation="horizontal">
<Switch id="auto-deploy" />
<FieldLabel htmlFor="auto-deploy">Deploy on every push</FieldLabel>
</Field>The thumb travels on a spring and stretches while pressed, the way a physical toggle gives under your finger. With reduced motion it simply snaps into place.
Examples
Switch or checkbox?
A switch is a light switch: flipping it changes something immediately — no Save button, no confirmation. A checkbox is a form answer: nothing happens until the form is submitted. Here the switch restyles the card the instant it changes.
"use client"
import * as React from "react"
import { MoonIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"
export default function SwitchImmediate() {
const [dark, setDark] = React.useState(false)
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Field orientation="horizontal">
<Switch id="preview-dark" checked={dark} onCheckedChange={setDark} />
<FieldLabel htmlFor="preview-dark">Dark preview</FieldLabel>
</Field>
{/* The switch applies instantly — no Save button, no confirmation. */}
<div
className={cn(
dark ? "dark" : "light",
"flex items-center justify-between gap-4 rounded-xl border bg-card p-4 text-card-foreground shadow-raised transition-colors duration-base"
)}
>
<div className="flex items-center gap-3">
<span className="flex size-9 items-center justify-center rounded-lg bg-brand-3 text-brand-11">
<MoonIcon className="size-4" />
</span>
<div className="flex flex-col">
<span className="text-sm font-medium">Night shift</span>
<span className="text-xs text-muted-foreground">
Starts at 22:00
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="success">Active</Badge>
<Button size="xs" variant="outline">
Edit
</Button>
</div>
</div>
</div>
)
}Settings list
The most common home for switches: one setting per row, the label and a short description on the left, the switch on the right where the eye lands after reading. Separators keep long lists scannable.
Let reviewers leave comments on preview deployments.
Keep clients on the version they loaded until they refresh.
Anyone with the link can read build output.
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
const settings = [
{
id: "preview-comments",
title: "Preview comments",
description: "Let reviewers leave comments on preview deployments.",
checked: true,
},
{
id: "skew-protection",
title: "Skew protection",
description: "Keep clients on the version they loaded until they refresh.",
checked: true,
},
{
id: "public-logs",
title: "Public build logs",
description: "Anyone with the link can read build output.",
checked: false,
},
]
export default function SwitchSettings() {
return (
<FieldGroup className="w-full max-w-md gap-4 rounded-xl border bg-card p-5 shadow-raised">
{settings.map((setting, index) => (
<div key={setting.id} className="flex flex-col gap-4">
{index > 0 && <FieldSeparator />}
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor={setting.id}>{setting.title}</FieldLabel>
<FieldDescription>{setting.description}</FieldDescription>
</FieldContent>
<Switch id={setting.id} defaultChecked={setting.checked} />
</Field>
</div>
))}
</FieldGroup>
)
}Sizes
size="sm" fits dense tables and compact panels; the default size suits forms and settings pages.
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function SwitchSizes() {
return (
<div className="flex w-80 flex-col gap-4">
<Field orientation="horizontal">
<Switch id="size-sm" size="sm" defaultChecked />
<FieldLabel htmlFor="size-sm" className="text-body-xs">
Small — dense lists and table rows
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Switch id="size-default" defaultChecked />
<FieldLabel htmlFor="size-default" className="font-normal">
Default — settings and forms
</FieldLabel>
</Field>
</div>
)
}States
Disabled switches keep their position so people can still read the current value. When a setting is required, mark the switch aria-invalid and explain why in a FieldError.
import {
Field,
FieldContent,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
export default function SwitchStates() {
return (
<FieldGroup className="w-80 gap-4">
<Field orientation="horizontal" data-disabled>
<Switch id="switch-disabled-off" disabled />
<FieldLabel htmlFor="switch-disabled-off" className="font-normal">
Disabled
</FieldLabel>
</Field>
<Field orientation="horizontal" data-disabled>
<Switch id="switch-disabled-on" disabled defaultChecked />
<FieldLabel htmlFor="switch-disabled-on" className="font-normal">
Disabled and on
</FieldLabel>
</Field>
<Field orientation="horizontal" data-invalid>
<Switch id="switch-invalid" aria-invalid />
<FieldContent>
<FieldLabel htmlFor="switch-invalid">
Two-factor authentication
</FieldLabel>
<FieldError>
Your organization requires 2FA for all members.
</FieldError>
</FieldContent>
</Field>
</FieldGroup>
)
}Guidelines
When to use
- For settings that apply immediately: notifications, integrations, feature toggles, dark mode.
- For binary states people understand without extra explanation — on or off.
When not to use
- In forms that are submitted — use a Checkbox.
- For choices that aren't on/off, such as Monthly vs Yearly — use a Radio Group or Toggle Group.
- For actions with consequences that need confirmation — use a Button and an Alert Dialog.
Labels
Label the setting, not the state. The switch already shows on and off, so the label should name what is being turned on — never Enable / Disable, and never text that changes when the switch flips.
Email notifications
Notifications are on — click to turn off
Feedback
Because a switch acts immediately, confirm the result when it isn't visible on screen — a Toast such as Preview comments enabled — and undo the change (with an explanation) if the request fails.
Accessibility
The switch renders role="switch" with aria-checked, plus a hidden checkbox input for forms. A sibling FieldLabel with htmlFor both names it and toggles it on click.
| Key | Behavior |
|---|---|
Tab | Moves focus to the switch. Focus shows a 2px ring offset from the track. |
SpaceEnter | Toggles the switch. |
- Boundaries. The off track uses the
inputtoken, which meets 3:1 against the page (WCAG 1.4.11); the on track uses the brand solid. The thumb's position — not color alone — carries the state. - Target size. The 36×20px track has a transparent hit area extending it to 44×36px.
- Motion. Under
prefers-reduced-motionthe thumb changes position and width instantly, without the spring.
API reference
Switch
Renders a <span role="switch"> with a thumb and a hidden <input type="checkbox">. Accepts every prop of Base UI's Switch.Root.