Toggle Switch
Accessible switch with a springy knob, shown in a settings list.
Email notifications
Get a summary of activity every morning.
Push notifications
Real-time alerts on your devices.
Weekly report
Metrics and trends every Monday.
components/ui/toggle-switch.tsx
"use client";
import { motion } from "motion/react";
import { useState } from "react";
type ToggleSwitchProps = {
checked: boolean;
onChange: (checked: boolean) => void;
label?: string;
};
export function ToggleSwitch({ checked, onChange, label }: ToggleSwitchProps) {
return (
<button
role="switch"
aria-checked={checked}
aria-label={label}
onClick={() => onChange(!checked)}
className={`flex h-7 w-12 shrink-0 items-center rounded-full p-1 transition-colors duration-300 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-blue-600/20 ${
checked ? "justify-end bg-blue-600" : "justify-start bg-zinc-200"
}`}
>
<motion.span
layout
transition={{ type: "spring", stiffness: 600, damping: 32 }}
className="size-5 rounded-full bg-white shadow-md"
/>
</button>
);
}
const settings = [
{ id: "email", title: "Email notifications", text: "Get a summary of activity every morning." },
{ id: "push", title: "Push notifications", text: "Real-time alerts on your devices." },
{ id: "weekly", title: "Weekly report", text: "Metrics and trends every Monday." },
];
export default function Demo() {
const [state, setState] = useState<Record<string, boolean>>({ email: true, push: false, weekly: true });
return (
<div className="w-full max-w-md divide-y divide-zinc-100 rounded-2xl border border-zinc-200 bg-white">
{settings.map((s) => (
<div key={s.id} className="flex items-center justify-between gap-6 p-5">
<div>
<p className="text-sm font-medium text-zinc-900">{s.title}</p>
<p className="text-sm text-zinc-500">{s.text}</p>
</div>
<ToggleSwitch label={s.title} checked={state[s.id]} onChange={(v) => setState((p) => ({ ...p, [s.id]: v }))} />
</div>
))}
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/toggle-switch.tsxcomponents/ui/toggle-switch.tsx"use client"; import { motion } from "motion/react"; import { useState } from "react"; type ToggleSwitchProps = { checked: boolean; onChange: (checked: boolean) => void; label?: string; }; export function ToggleSwitch({ checked, onChange, label }: ToggleSwitchProps) { return ( <button role="switch" aria-checked={checked} aria-label={label} onClick={() => onChange(!checked)} className={`flex h-7 w-12 shrink-0 items-center rounded-full p-1 transition-colors duration-300 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-blue-600/20 ${ checked ? "justify-end bg-blue-600" : "justify-start bg-zinc-200" }`} > <motion.span layout transition={{ type: "spring", stiffness: 600, damping: 32 }} className="size-5 rounded-full bg-white shadow-md" /> </button> ); } const settings = [ { id: "email", title: "Email notifications", text: "Get a summary of activity every morning." }, { id: "push", title: "Push notifications", text: "Real-time alerts on your devices." }, { id: "weekly", title: "Weekly report", text: "Metrics and trends every Monday." }, ]; export default function Demo() { const [state, setState] = useState<Record<string, boolean>>({ email: true, push: false, weekly: true }); return ( <div className="w-full max-w-md divide-y divide-zinc-100 rounded-2xl border border-zinc-200 bg-white"> {settings.map((s) => ( <div key={s.id} className="flex items-center justify-between gap-6 p-5"> <div> <p className="text-sm font-medium text-zinc-900">{s.title}</p> <p className="text-sm text-zinc-500">{s.text}</p> </div> <ToggleSwitch label={s.title} checked={state[s.id]} onChange={(v) => setState((p) => ({ ...p, [s.id]: v }))} /> </div> ))} </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.