Radio Segmented Toggle
Original · freesegmented control radios
byWeb InnoventixReact + Tailwind
tglradiosegmentedtoggles
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-radio-segmented.jsontgl-radio-segmented.tsx
"use client";
import { useId, useState } from "react";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type SegOption<T extends string> = {
value: T;
label: string;
hint?: string;
icon?: ReactNode;
};
type SegSize = "sm" | "md" | "lg";
const SIZE: Record<SegSize, { text: string; pad: string; gap: string }> = {
sm: { text: "text-xs", pad: "px-3 py-1.5", gap: "gap-1.5" },
md: { text: "text-sm", pad: "px-4 py-2", gap: "gap-2" },
lg: { text: "text-[15px]", pad: "px-5 py-2.5", gap: "gap-2.5" },
};
function labelOf(
opts: ReadonlyArray<{ value: string; label: string }>,
v: string,
): string {
return opts.find((o) => o.value === v)?.label ?? v;
}
function SegmentedControl<T extends string>({
name,
label,
options,
value,
onChange,
size = "md",
fullWidth = false,
}: {
name: string;
label: string;
options: ReadonlyArray<SegOption<T>>;
value: T;
onChange: (value: T) => void;
size?: SegSize;
fullWidth?: boolean;
}) {
const pillId = useId();
const reduce = useReducedMotion();
const s = SIZE[size];
const spring = reduce
? { duration: 0 }
: { type: "spring" as const, stiffness: 380, damping: 30, mass: 0.7 };
return (
<fieldset className="m-0 min-w-0 border-0 p-0">
<legend className="sr-only">{label}</legend>
<div
className={`${
fullWidth ? "flex w-full" : "inline-flex"
} gap-1 rounded-xl bg-zinc-100 p-1 ring-1 ring-inset ring-zinc-200/80 dark:bg-zinc-800/80 dark:ring-white/5`}
>
{options.map((opt) => {
const selected = opt.value === value;
return (
<label
key={opt.value}
className={`relative flex ${
fullWidth ? "flex-1" : ""
} cursor-pointer select-none`}
>
<input
type="radio"
name={name}
value={opt.value}
checked={selected}
onChange={() => onChange(opt.value)}
className="peer sr-only"
/>
{selected && (
<motion.span
aria-hidden="true"
layoutId={pillId}
transition={spring}
className="absolute inset-0 rounded-lg bg-white shadow-sm ring-1 ring-black/5 dark:bg-zinc-700 dark:ring-white/10"
/>
)}
<span
className={`relative z-10 flex w-full items-center justify-center ${s.gap} ${s.pad} ${s.text} rounded-lg font-medium transition-colors ${
selected
? "text-zinc-900 dark:text-white"
: "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
} peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-1 peer-focus-visible:ring-offset-zinc-100 dark:peer-focus-visible:ring-indigo-400 dark:peer-focus-visible:ring-offset-zinc-800`}
>
{opt.icon}
<span>{opt.label}</span>
{opt.hint && (
<span
className={`ml-0.5 rounded px-1.5 py-0.5 text-[10px] font-semibold leading-none ${
selected
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
: "bg-zinc-200/70 text-zinc-500 dark:bg-zinc-700/70 dark:text-zinc-400"
}`}
>
{opt.hint}
</span>
)}
</span>
</label>
);
})}
</div>
</fieldset>
);
}
function ListIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="4.5" cy="6" r="1" />
<circle cx="4.5" cy="12" r="1" />
<circle cx="4.5" cy="18" r="1" />
<line x1="9" y1="6" x2="20" y2="6" />
<line x1="9" y1="12" x2="20" y2="12" />
<line x1="9" y1="18" x2="20" y2="18" />
</svg>
);
}
function GridIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3.5" y="3.5" width="7" height="7" rx="1.5" />
<rect x="13.5" y="3.5" width="7" height="7" rx="1.5" />
<rect x="3.5" y="13.5" width="7" height="7" rx="1.5" />
<rect x="13.5" y="13.5" width="7" height="7" rx="1.5" />
</svg>
);
}
function BoardIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3.5" y="4" width="4.5" height="16" rx="1.5" />
<rect x="9.75" y="4" width="4.5" height="11" rx="1.5" />
<rect x="16" y="4" width="4.5" height="14" rx="1.5" />
</svg>
);
}
function TimelineIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3" y="5" width="12" height="3.2" rx="1.6" />
<rect x="7" y="10.4" width="14" height="3.2" rx="1.6" />
<rect x="4.5" y="15.8" width="9" height="3.2" rx="1.6" />
</svg>
);
}
function SunIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" />
<line x1="12" y1="2.5" x2="12" y2="4.5" />
<line x1="12" y1="19.5" x2="12" y2="21.5" />
<line x1="4.2" y1="4.2" x2="5.7" y2="5.7" />
<line x1="18.3" y1="18.3" x2="19.8" y2="19.8" />
<line x1="2.5" y1="12" x2="4.5" y2="12" />
<line x1="19.5" y1="12" x2="21.5" y2="12" />
<line x1="4.2" y1="19.8" x2="5.7" y2="18.3" />
<line x1="18.3" y1="5.7" x2="19.8" y2="4.2" />
</svg>
);
}
function MoonIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8Z" />
</svg>
);
}
function MonitorIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3" y="4" width="18" height="12" rx="2" />
<line x1="8" y1="20" x2="16" y2="20" />
<line x1="12" y1="16" x2="12" y2="20" />
</svg>
);
}
type Billing = "monthly" | "quarterly" | "annual";
type BoardView = "list" | "grid" | "board" | "timeline";
type Appearance = "light" | "dark" | "system";
type Speed = "standard" | "express" | "priority";
const CSS = `
@keyframes tglseg-fadein {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes tglseg-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.72); }
}
.tglseg-enter { animation: tglseg-fadein 0.5s ease-out both; }
.tglseg-dot { animation: tglseg-pulse 1.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.tglseg-enter, .tglseg-dot { animation: none !important; }
}
`;
function Card({
title,
description,
readout,
children,
}: {
title: string;
description: string;
readout: string;
children: ReactNode;
}) {
return (
<div className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="mb-4 flex items-start justify-between gap-4">
<div className="min-w-0">
<h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
{title}
</h3>
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{description}
</p>
</div>
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-zinc-100 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
<span
className="tglseg-dot h-1.5 w-1.5 rounded-full bg-emerald-500"
aria-hidden="true"
/>
{readout}
</span>
</div>
{children}
</div>
);
}
export default function TglRadioSegmented() {
const [billing, setBilling] = useState<Billing>("monthly");
const [view, setView] = useState<BoardView>("grid");
const [appearance, setAppearance] = useState<Appearance>("system");
const [speed, setSpeed] = useState<Speed>("express");
const billingOptions: ReadonlyArray<SegOption<Billing>> = [
{ value: "monthly", label: "Monthly" },
{ value: "quarterly", label: "Quarterly" },
{ value: "annual", label: "Annual", hint: "-20%" },
];
const viewOptions: ReadonlyArray<SegOption<BoardView>> = [
{ value: "list", label: "List", icon: <ListIcon className="h-4 w-4 shrink-0" /> },
{ value: "grid", label: "Grid", icon: <GridIcon className="h-4 w-4 shrink-0" /> },
{ value: "board", label: "Board", icon: <BoardIcon className="h-4 w-4 shrink-0" /> },
{
value: "timeline",
label: "Timeline",
icon: <TimelineIcon className="h-4 w-4 shrink-0" />,
},
];
const appearanceOptions: ReadonlyArray<SegOption<Appearance>> = [
{ value: "light", label: "Light", icon: <SunIcon className="h-4 w-4 shrink-0" /> },
{ value: "dark", label: "Dark", icon: <MoonIcon className="h-4 w-4 shrink-0" /> },
{
value: "system",
label: "System",
icon: <MonitorIcon className="h-4 w-4 shrink-0" />,
},
];
const speedOptions: ReadonlyArray<SegOption<Speed>> = [
{ value: "standard", label: "Standard" },
{ value: "express", label: "Express" },
{ value: "priority", label: "Priority" },
];
return (
<section className="relative w-full overflow-hidden bg-zinc-50 px-6 py-20 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-50 sm:px-8 lg:py-28">
<style>{CSS}</style>
<div className="tglseg-enter mx-auto max-w-2xl">
<header className="mb-10 text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" aria-hidden="true" />
Segmented controls
</span>
<h2 className="mt-4 text-2xl font-semibold tracking-tight sm:text-3xl">
Pick one, cleanly
</h2>
<p className="mx-auto mt-3 max-w-md text-sm text-zinc-500 dark:text-zinc-400">
Accessible radio groups styled as segmented controls. Tab to focus a
group, then use the arrow keys to switch options.
</p>
</header>
<div className="space-y-5">
<Card
title="Billing cycle"
description="Choose how often your workspace is charged."
readout={labelOf(billingOptions, billing)}
>
<SegmentedControl
name="billing-cycle"
label="Billing cycle"
options={billingOptions}
value={billing}
onChange={setBilling}
size="md"
/>
</Card>
<Card
title="Board layout"
description="How tasks are arranged in the project view."
readout={labelOf(viewOptions, view)}
>
<SegmentedControl
name="board-layout"
label="Board layout"
options={viewOptions}
value={view}
onChange={setView}
size="md"
fullWidth
/>
</Card>
<Card
title="Appearance"
description="Theme applied across the editor and sidebar."
readout={labelOf(appearanceOptions, appearance)}
>
<SegmentedControl
name="appearance"
label="Appearance"
options={appearanceOptions}
value={appearance}
onChange={setAppearance}
size="sm"
/>
</Card>
<Card
title="Delivery speed"
description="Applies to every export queued from this project."
readout={labelOf(speedOptions, speed)}
>
<SegmentedControl
name="delivery-speed"
label="Delivery speed"
options={speedOptions}
value={speed}
onChange={setSpeed}
size="lg"
fullWidth
/>
</Card>
</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

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 Labels Toggle
Originalswitches with on/off labels inside

Switch Group Toggle
Originala settings list of switches

