Switch Labels Toggle
Original · freeswitches with on/off labels inside
byWeb InnoventixReact + Tailwind
tglswitchlabelstoggles
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/tgl-switch-labels.jsontgl-switch-labels.tsx
"use client";
import { useId, useState, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type SwitchSize = "sm" | "md" | "lg";
type SwitchTone = "indigo" | "emerald" | "violet" | "rose" | "sky" | "amber";
const SIZE: Record<SwitchSize, { track: string; thumb: string; text: string; on: string; off: string }> = {
sm: { track: "h-7 w-16 px-1", thumb: "h-5 w-5", text: "text-[10px]", on: "pl-2", off: "pr-2" },
md: { track: "h-9 w-[4.75rem] px-1", thumb: "h-7 w-7", text: "text-[11px]", on: "pl-2.5", off: "pr-2.5" },
lg: { track: "h-11 w-24 px-1.5", thumb: "h-8 w-8", text: "text-xs", on: "pl-3", off: "pr-3" },
};
const TONE: Record<SwitchTone, { on: string; ring: string }> = {
indigo: { on: "bg-indigo-600 dark:bg-indigo-500", ring: "focus-visible:ring-indigo-500" },
emerald: { on: "bg-emerald-600 dark:bg-emerald-500", ring: "focus-visible:ring-emerald-500" },
violet: { on: "bg-violet-600 dark:bg-violet-500", ring: "focus-visible:ring-violet-500" },
rose: { on: "bg-rose-600 dark:bg-rose-500", ring: "focus-visible:ring-rose-500" },
sky: { on: "bg-sky-600 dark:bg-sky-500", ring: "focus-visible:ring-sky-500" },
amber: { on: "bg-amber-500 dark:bg-amber-400", ring: "focus-visible:ring-amber-500" },
};
function IconCheck({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
function IconX({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
);
}
function IconSun({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
);
}
function IconMoon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
</svg>
);
}
interface LabelSwitchProps {
checked: boolean;
onCheckedChange: (value: boolean) => void;
onLabel: string;
offLabel: string;
labelledBy?: string;
describedBy?: string;
ariaLabel?: string;
tone?: SwitchTone;
size?: SwitchSize;
disabled?: boolean;
renderIcon?: (checked: boolean) => ReactNode;
}
function LabelSwitch({
checked,
onCheckedChange,
onLabel,
offLabel,
labelledBy,
describedBy,
ariaLabel,
tone = "indigo",
size = "md",
disabled = false,
renderIcon,
}: LabelSwitchProps) {
const reduce = useReducedMotion();
const s = SIZE[size];
const t = TONE[tone];
const spring = reduce
? { duration: 0 }
: { type: "spring" as const, stiffness: 520, damping: 34, mass: 0.7 };
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-labelledby={labelledBy}
aria-describedby={describedBy}
aria-label={ariaLabel}
disabled={disabled}
onClick={() => onCheckedChange(!checked)}
className={`relative inline-flex shrink-0 items-center rounded-full outline-none ring-1 ring-inset ring-black/5 transition-colors duration-200 dark:ring-white/10 ${
checked ? t.on : "bg-slate-200 dark:bg-slate-700"
} ${s.track} ${checked ? "justify-end" : "justify-start"} ${t.ring} cursor-pointer focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-offset-slate-900`}
>
<span
key={checked ? "on" : "off-hidden"}
aria-hidden="true"
className={`tglsl-anim pointer-events-none absolute inset-y-0 left-0 flex items-center font-semibold uppercase tracking-wider text-white transition-opacity duration-200 ${s.text} ${s.on} ${
checked ? "animate-[tglsl-label-in_240ms_ease-out] opacity-100" : "opacity-0"
}`}
>
{onLabel}
</span>
<span
key={checked ? "off-hidden" : "off"}
aria-hidden="true"
className={`tglsl-anim pointer-events-none absolute inset-y-0 right-0 flex items-center font-semibold uppercase tracking-wider text-slate-600 transition-opacity duration-200 dark:text-slate-300 ${s.text} ${s.off} ${
checked ? "opacity-0" : "animate-[tglsl-label-in_240ms_ease-out] opacity-100"
}`}
>
{offLabel}
</span>
<motion.span
layout
transition={spring}
aria-hidden="true"
className={`z-10 flex items-center justify-center rounded-full bg-white text-slate-700 shadow-[0_1px_3px_rgba(15,23,42,0.28)] ${s.thumb} dark:bg-slate-50`}
>
{renderIcon ? renderIcon(checked) : null}
</motion.span>
</button>
);
}
interface PrefRow {
key: string;
title: string;
desc: string;
onLabel: string;
offLabel: string;
tone: SwitchTone;
icons: boolean;
}
const PREF_ROWS: PrefRow[] = [
{ key: "push", title: "Push notifications", desc: "Alerts for mentions, replies, and system events.", onLabel: "Live", offLabel: "Off", tone: "emerald", icons: true },
{ key: "renew", title: "Auto-renew plan", desc: "Charge the card on file when your billing cycle ends.", onLabel: "On", offLabel: "Off", tone: "indigo", icons: false },
{ key: "profile", title: "Public profile", desc: "Let anyone with the link view your activity page.", onLabel: "On", offLabel: "Off", tone: "violet", icons: false },
{ key: "marketing", title: "Marketing emails", desc: "Product tips, changelog digests, and the odd offer.", onLabel: "Yes", offLabel: "No", tone: "amber", icons: false },
];
export default function TglSwitchLabels() {
const baseId = useId();
const [demo, setDemo] = useState<Record<SwitchSize, boolean>>({ sm: true, md: false, lg: true });
const [prefs, setPrefs] = useState<Record<string, boolean>>({ push: true, renew: true, profile: false, marketing: false });
const [themeDark, setThemeDark] = useState(false);
const sizes: SwitchSize[] = ["sm", "md", "lg"];
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-6 py-16 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8 md:py-24">
<style>{`
@keyframes tglsl-label-in {
0% { opacity: 0; transform: translateY(2px) scale(0.9); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.tglsl-anim { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-3xl">
<header className="mb-12 md:mb-16">
<span className="inline-flex items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
Toggles
</span>
<h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
Switches with labels inside
</h2>
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
The current state is written into the track, so there is never a guess about which way is on. Fully keyboard operable, screen-reader labelled, and legible in both themes.
</p>
</header>
{/* Sizes */}
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-8">
<h3 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Three sizes
</h3>
<div className="mt-6 flex flex-wrap items-end gap-x-10 gap-y-8">
{sizes.map((sz) => {
const id = `${baseId}-size-${sz}`;
return (
<div key={sz} className="flex flex-col items-center gap-3">
<LabelSwitch
size={sz}
tone="indigo"
onLabel="On"
offLabel="Off"
checked={demo[sz]}
onCheckedChange={(v) => setDemo((p) => ({ ...p, [sz]: v }))}
labelledBy={id}
/>
<span id={id} className="text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
{sz}
</span>
</div>
);
})}
</div>
</div>
{/* Preferences */}
<div className="mt-8 rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="border-b border-slate-200 px-6 py-5 dark:border-slate-800 sm:px-8">
<h3 className="text-base font-semibold">Account preferences</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Changes save the moment you flip a switch.
</p>
</div>
<ul className="divide-y divide-slate-200 dark:divide-slate-800">
{PREF_ROWS.map((row) => {
const titleId = `${baseId}-${row.key}-title`;
const descId = `${baseId}-${row.key}-desc`;
const on = prefs[row.key];
return (
<li key={row.key} className="flex items-center justify-between gap-6 px-6 py-5 sm:px-8">
<div className="min-w-0">
<p id={titleId} className="text-sm font-semibold text-slate-800 dark:text-slate-100">
{row.title}
</p>
<p id={descId} className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
{row.desc}
</p>
</div>
<LabelSwitch
tone={row.tone}
size="md"
onLabel={row.onLabel}
offLabel={row.offLabel}
checked={on}
onCheckedChange={(v) => setPrefs((p) => ({ ...p, [row.key]: v }))}
labelledBy={titleId}
describedBy={descId}
renderIcon={
row.icons
? (c) =>
c ? (
<IconCheck className="h-3.5 w-3.5 text-emerald-600" />
) : (
<IconX className="h-3.5 w-3.5 text-slate-400" />
)
: undefined
}
/>
</li>
);
})}
</ul>
</div>
{/* Theme */}
<div className="mt-8 grid gap-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:grid-cols-[1fr_auto] sm:items-center sm:p-8">
<div>
<h3 className="text-base font-semibold">Appearance</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Preview panel updates instantly — currently{" "}
<span className="font-semibold text-slate-700 dark:text-slate-200">
{themeDark ? "Dark" : "Light"} mode
</span>
.
</p>
<div
className={`mt-5 flex items-center gap-3 rounded-xl border p-4 transition-colors duration-300 ${
themeDark ? "border-slate-700 bg-slate-800" : "border-slate-200 bg-slate-50"
}`}
>
<span
className={`flex h-9 w-9 items-center justify-center rounded-lg text-lg font-bold transition-colors duration-300 ${
themeDark ? "bg-slate-700 text-sky-300" : "bg-white text-sky-600 shadow-sm"
}`}
>
Aa
</span>
<div className="space-y-1.5">
<span className={`block h-2 w-32 rounded-full transition-colors duration-300 ${themeDark ? "bg-slate-600" : "bg-slate-300"}`} />
<span className={`block h-2 w-20 rounded-full transition-colors duration-300 ${themeDark ? "bg-slate-700" : "bg-slate-200"}`} />
</div>
</div>
</div>
<div className="flex justify-start sm:justify-end">
<LabelSwitch
tone="sky"
size="lg"
onLabel="Dark"
offLabel="Light"
checked={themeDark}
onCheckedChange={setThemeDark}
ariaLabel="Toggle dark mode preview"
renderIcon={(c) =>
c ? <IconMoon className="h-4 w-4 text-sky-600" /> : <IconSun className="h-4 w-4 text-amber-500" />
}
/>
</div>
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Checkbox Set Toggle
Originaldefault/checked/indeterminate/disabled checkboxes

Checkbox Custom Toggle
Originalcustom animated tick checkboxes

Checkbox Card Toggle
Originalselectable card checkboxes

Checkbox Indeterminate Toggle
Originalparent/child indeterminate checkboxes

Checkbox List Toggle
Originalcheckbox list with select-all

Radio Set Toggle
Originalstyled radio button group

Radio Cards Toggle
Originalselectable radio cards

Radio Segmented Toggle
Originalsegmented control radios

Switch Basic Toggle
Originalbasic on/off switches in sizes

Switch iOS Toggle
OriginaliOS-style switches
Switch Icons Toggle
Originalswitches with sun/moon style icons

Switch Group Toggle
Originala settings list of switches

