React Switch Component Examples
Copy-paste React Switch examples: settings toggle, dark mode flip, react-hook-form integration, disabled state, label pairing, and a notification panel pattern.
A switch is the right control for a single binary preference that takes effect immediately — turn on notifications, flip the theme to dark, enable email digests, opt into beta features. The Switch in Drivn is a single ~30 line client component built on a native <button type="button" role="switch">, with a track that swaps between bg-primary and bg-border and a white thumb that translates 22 pixels across the pill. There is no Radix wrapper, no headless toggle layer, no class-variance-authority — just cn() from @/utils/cn and a controlled checked + onChange API.
Every example on this page imports Switch from @/components/ui/switch — the path the Drivn CLI installs it under — and pairs it with React.useState inside a 'use client' component. The parent owns the boolean state, the Switch fires onChange(!checked) when clicked, and the aria-checked attribute updates so screen readers announce the new state. Every snippet is TypeScript because Drivn ships TypeScript-only — .tsx source files are the only thing the registry produces.
The examples below cover a labelled settings toggle inside a Card, a dark mode flip wired to next-themes, a react-hook-form integration via Controller, a disabled gate that visually communicates "this preference is locked", a stacked list of notification toggles, and a Switch paired with a Label for proper form semantics.
A labelled settings toggle inside a Card
The canonical Switch use is a single boolean preference inside a settings panel — "Enable email notifications", "Show desktop alerts", "Auto-save drafts". Wrap the Switch and its Label in a flex row with justify-between, drop the whole thing inside a Card, and the visual hierarchy lands without any extra styling. The label sits on the left, the Switch sits on the right, and the row stays compact at any width.
Wire the label's htmlFor to the Switch's id so clicking the text flips the toggle. Because the Switch renders a <button role="switch"> rather than a checkbox input, a regular <label htmlFor> does not associate automatically — pass the id explicitly and let the Label component's own click handler delegate to the button. The boolean state lives in React.useState in the parent component, and the onChange callback receives the new value directly.
1 "use client" 2 3 import * as React from "react" 4 import { Switch } from "@/components/ui/switch" 5 import { Label } from "@/components/ui/label" 6 7 export default function NotificationsToggle() { 8 const [enabled, setEnabled] = React.useState(true) 9 10 return ( 11 <div className="flex items-center justify-between"> 12 <Label htmlFor="notifications">Email notifications</Label> 13 <Switch 14 id="notifications" 15 checked={enabled} 16 onChange={setEnabled} 17 /> 18 </div> 19 ) 20 }
A dark mode flip wired to next-themes
The Drivn site itself uses data-theme attribute switching via next-themes, and a Switch is the natural control for "flip between light and dark". Pull the current theme and the setTheme function from useTheme(), derive a boolean from theme === 'dark', and pass setTheme('dark') or setTheme('light') from the onChange callback. The Switch reflects the current theme on hydration and flips it on click — no extra wiring.
Guard the initial render with mounted because next-themes reads localStorage on the client only — rendering the Switch in its dark state on a server-rendered light HTML page causes a one-frame flash. The mounted check is a one-liner with React.useEffect, and it keeps the Switch hidden until the theme is known. Drop this inside a Header right-aligned cluster next to the Avatar for a clean dark-mode control.
1 "use client" 2 3 import * as React from "react" 4 import { useTheme } from "next-themes" 5 import { Switch } from "@/components/ui/switch" 6 7 export default function ThemeToggle() { 8 const { theme, setTheme } = useTheme() 9 const [mounted, setMounted] = React.useState(false) 10 11 React.useEffect(() => setMounted(true), []) 12 13 if (!mounted) return null 14 15 return ( 16 <Switch 17 checked={theme === "dark"} 18 onChange={(on) => setTheme(on ? "dark" : "light")} 19 /> 20 ) 21 }
react-hook-form integration via Controller
The Drivn Switch is controlled-only and renders a button rather than a form input, so a plain register("fieldName") from react-hook-form will not bridge it directly. Use Controller instead: pass the name, the form control, and a render function that receives field.value and field.onChange, then plug those into the Switch's checked and onChange props. The form state lives entirely inside react-hook-form, validation runs through Zod or your resolver of choice, and the Switch participates in handleSubmit like any other Input.
This is the pattern to reach for whenever a Switch sits inside a multi-field settings form — preferences page, notification settings, feature flags admin, beta opt-in. The boolean value is part of the form schema, gets the same dirty/touched/error treatment as text inputs, and submits in the same payload. Pair this with a Button of variant default for the form submit.
1 "use client" 2 3 import { Controller, useForm } from "react-hook-form" 4 import { Switch } from "@/components/ui/switch" 5 import { Label } from "@/components/ui/label" 6 import { Button } from "@/components/ui/button" 7 8 type FormValues = { marketing: boolean } 9 10 export default function PreferencesForm() { 11 const { control, handleSubmit } = useForm<FormValues>({ 12 defaultValues: { marketing: false }, 13 }) 14 15 return ( 16 <form 17 onSubmit={handleSubmit((v) => console.log(v))} 18 className="space-y-4" 19 > 20 <div className="flex items-center justify-between"> 21 <Label htmlFor="marketing">Marketing emails</Label> 22 <Controller 23 control={control} 24 name="marketing" 25 render={({ field }) => ( 26 <Switch 27 id="marketing" 28 checked={field.value} 29 onChange={field.onChange} 30 /> 31 )} 32 /> 33 </div> 34 <Button type="submit">Save</Button> 35 </form> 36 ) 37 }
A disabled gate that locks the preference
When a Switch should appear in the UI but cannot be toggled — the user lacks the role, the preference depends on another setting being enabled first, or the feature is region-gated — pass disabled on the Switch. The underlying button receives the native disabled attribute, the click handler stops firing, and the focus ring still appears so keyboard users can see where they are without accidentally flipping a locked control. The visual treatment falls out of native button disabled styling — pair it with an opacity-50 wrapper on the row if you want the entire setting to recede.
The disabled prop is part of React.ButtonHTMLAttributes<HTMLButtonElement> and passes through via the ...props spread inside the Switch component, so you can pass any other native button attribute the same way — aria-label, data-testid, form, name. The Drivn Switch is intentionally close to the metal so these passthroughs feel native rather than escaped. Surface a Tooltip over a disabled Switch to explain why it is locked.
1 "use client" 2 3 import * as React from "react" 4 import { Switch } from "@/components/ui/switch" 5 import { Label } from "@/components/ui/label" 6 7 export default function LockedToggle() { 8 return ( 9 <div className="flex items-center justify-between opacity-60"> 10 <Label htmlFor="beta">Beta features (admin only)</Label> 11 <Switch 12 id="beta" 13 checked={false} 14 disabled 15 aria-label="Beta features locked" 16 /> 17 </div> 18 ) 19 }
A stacked notification preferences panel
For a settings page that exposes several related toggles — push notifications, email digest, weekly summary, mentions, replies — render the Switches as a stacked list inside a Card with Separator lines between rows. Each row is the same flex-between pattern as the single-toggle example, the labels describe the preference in one phrase, and the Switches all live in a single state object keyed by preference name. A single setPrefs((p) => ({ ...p, [key]: !p[key] })) updater handles every toggle.
This is the right structure for the notification preferences screen of any product — the visual rhythm of label + switch pairs is what users expect for binary preferences, and the per-row updater keeps the state shape clean as you add or remove options. Drop the card inside a Tabs panel grouped under "Notifications" alongside other settings categories.
1 "use client" 2 3 import * as React from "react" 4 import { Switch } from "@/components/ui/switch" 5 import { Label } from "@/components/ui/label" 6 7 const options = [ 8 { key: "push", label: "Push notifications" }, 9 { key: "email", label: "Email digest" }, 10 { key: "weekly", label: "Weekly summary" }, 11 { key: "mentions", label: "Mentions" }, 12 ] as const 13 14 type Key = (typeof options)[number]["key"] 15 16 export default function NotificationsPanel() { 17 const [prefs, setPrefs] = React.useState<Record<Key, boolean>>({ 18 push: true, 19 email: false, 20 weekly: true, 21 mentions: true, 22 }) 23 24 return ( 25 <div className="space-y-4"> 26 {options.map(({ key, label }) => ( 27 <div key={key} className="flex items-center justify-between"> 28 <Label htmlFor={key}>{label}</Label> 29 <Switch 30 id={key} 31 checked={prefs[key]} 32 onChange={(on) => setPrefs((p) => ({ ...p, [key]: on }))} 33 /> 34 </div> 35 ))} 36 </div> 37 ) 38 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
Pass the initial state through useState — useState(true) — and the Switch renders with bg-primary on the track and the thumb translated to translate-x-[25px]. There is no defaultChecked prop on the Drivn Switch because the component is controlled-only; the caller owns the boolean and the Switch reflects it. If you need uncontrolled-style behaviour, wrap the Switch in a small parent component that owns the useState internally and exposes its own narrower API to the rest of your app.
Yes, via the Controller component. The Switch renders a button rather than a checkbox input, so a plain register call from react-hook-form will not bridge it. Wrap the Switch in Controller, pass name, control, and a render function that receives { field }, and plug field.value into checked and field.onChange into onChange. The Switch then participates in handleSubmit, validation, and dirty/touched tracking like any other react-hook-form field.
Two options. The first is a visible label rendered next to the Switch with the Label component — pass id on the Switch and htmlFor on the Label so a click on the text flips the toggle. The second is aria-label or aria-labelledby on the Switch itself when the visible context already explains the control (an icon-only toggle inside a toolbar, for example). The Switch already sets role="switch" and aria-checked, so screen readers announce the on/off state automatically — the label only needs to describe what the switch controls.
You normally do not need to — the Switch already applies transition-transform duration-200 on the thumb span, so flipping the checked prop animates the translate-x change for 200 milliseconds. To customise the timing, pass a className to the Switch root that overrides the duration via Tailwind utilities (the cn() merge resolves conflicts) or copy the registry source from packages/drivn/src/registry/components/switch.ts and edit the duration in the thumb styles entry directly.
The WAI-ARIA Switch pattern is a button with role="switch" and aria-checked, not a checkbox. Buttons announce as buttons by default and let role="switch" override the announcement to "switch", which is the correct semantic for a single binary preference. Checkboxes announce as "checked/unchecked" which reads more naturally for multi-select scenarios. The Drivn Switch follows the WAI-ARIA Switch pattern, which is also what @radix-ui/react-switch implements under the shadcn/ui wrapper.
Two layers. First, hydrate the initial state from your server data — pass the boolean from a server component into a client wrapper that holds it in useState, or fetch it with SWR / React Query and gate the Switch render on the loading state. Second, persist on change — call your API inside the onChange callback, optimistically update the local state, and roll back on error. Pair this with a Toast notification to tell the user when the preference saved successfully or failed.

