Toggle Group
Provides a shared state to a series of toggle buttons. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export default function Particle() {
return (
<ToggleGroup defaultValue={["bold"]}>
<ToggleGroupItem aria-label="Toggle bold" value="bold">
<BoldIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Toggle italic" value="italic">
<ItalicIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Toggle underline" value="underline">
<UnderlineIcon />
</ToggleGroupItem>
</ToggleGroup>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/toggle-group
Usage
import { Toggle, ToggleGroup } from "@/components/ui/toggle-group"<ToggleGroup>
<Toggle>Bold</Toggle>
<Toggle>Italic</Toggle>
<Toggle>Underline</Toggle>
</ToggleGroup>Examples
Sizes
Renders sm, default, and lg size variants side by side to demonstrate scale options across layouts.
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export function Pattern() {
return (
<div className="flex flex-col items-center justify-center gap-4">
<ToggleGroup defaultValue={["top"]} size="sm" variant="outline">
<ToggleGroupItem value="top">Top</ToggleGroupItem>
<ToggleGroupItem value="bottom">Bottom</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup defaultValue={["top"]} variant="outline">
<ToggleGroupItem value="top">Top</ToggleGroupItem>
<ToggleGroupItem value="bottom">Bottom</ToggleGroupItem>
</ToggleGroup>
</div>
);
}
With Outline Toggles
Uses the outline variant for each toggle, making the group feel more like a segmented button bar against light surfaces.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
ToggleGroupSeparator,
} from "@/components/ui/toggle-group";
export default function Particle() {
return (
<ToggleGroup defaultValue={["bold"]} variant="outline">
<ToggleGroupItem aria-label="Toggle bold" value="bold">
<BoldIcon />
</ToggleGroupItem>
<ToggleGroupSeparator />
<ToggleGroupItem aria-label="Toggle italic" value="italic">
<ItalicIcon />
</ToggleGroupItem>
<ToggleGroupSeparator />
<ToggleGroupItem aria-label="Toggle underline" value="underline">
<UnderlineIcon />
</ToggleGroupItem>
</ToggleGroup>
);
}
Vertical
Set orientation="vertical" to stack toggles top-to-bottom — common in side-panel formatting controls.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
ToggleGroupSeparator,
} from "@/components/ui/toggle-group";
export default function Particle() {
return (
<ToggleGroup
defaultValue={["bold"]}
orientation="vertical"
variant="outline"
>
<ToggleGroupItem aria-label="Toggle bold" value="bold">
<BoldIcon />
</ToggleGroupItem>
<ToggleGroupSeparator orientation="horizontal" />
<ToggleGroupItem aria-label="Toggle italic" value="italic">
<ItalicIcon />
</ToggleGroupItem>
<ToggleGroupSeparator orientation="horizontal" />
<ToggleGroupItem aria-label="Toggle underline" value="underline">
<UnderlineIcon />
</ToggleGroupItem>
</ToggleGroup>
);
}
Multiple selection
With type="multiple", users can activate more than one toggle at a time — suitable for text formatting options like bold + italic.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export default function Particle() {
return (
<ToggleGroup defaultValue={["bold"]} multiple>
<ToggleGroupItem aria-label="Toggle bold" value="bold">
<BoldIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Toggle italic" value="italic">
<ItalicIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Toggle underline" value="underline">
<UnderlineIcon />
</ToggleGroupItem>
</ToggleGroup>
);
}
Text Alignment
Icon-only toggles for left, center, right, and justify alignment — a standard rich-text editor control.
import {
AlignCenterIcon,
AlignLeftIcon,
AlignRightIcon,
MenuIcon,
} from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<ToggleGroup defaultValue={["left"]}>
<ToggleGroupItem aria-label="Align left" value="left">
<AlignLeftIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Align center" value="center">
<AlignCenterIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Align right" value="right">
<AlignRightIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Justify" value="justify">
<MenuIcon />
</ToggleGroupItem>
</ToggleGroup>
</div>
);
}
View Mode
A single-select group for switching between list and grid views, driving a layout change in the parent component.
import { Columns2Icon, LayoutGridIcon, ListIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<ToggleGroup defaultValue={["list"]} variant="outline">
<ToggleGroupItem aria-label="List view" value="list">
<ListIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Grid view" value="grid">
<LayoutGridIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Kanban view" value="kanban">
<Columns2Icon />
</ToggleGroupItem>
</ToggleGroup>
</div>
);
}
Color Theme
A compact group of color-swatch toggles for picking an accent theme, with a checkmark indicator on the active selection.
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<ToggleGroup defaultValue={["light"]} variant="outline">
<ToggleGroupItem aria-label="Light theme" value="light">
<SunIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="Dark theme" value="dark">
<MoonIcon />
</ToggleGroupItem>
<ToggleGroupItem aria-label="System theme" value="system">
<MonitorIcon />
</ToggleGroupItem>
</ToggleGroup>
</div>
);
}
Pricing Tier
A billing-period toggle (monthly / annual) that drives pricing display — a common SaaS pricing page pattern.
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<ToggleGroup defaultValue={["monthly"]} size="lg" variant="outline">
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
<ToggleGroupItem className="gap-2" value="yearly">
Yearly
<span className="rounded-full bg-primary px-2 py-0.5 font-medium text-primary-foreground text-xs">
Save 20%
</span>
</ToggleGroupItem>
</ToggleGroup>
</div>
);
}
Language Picker
Selects a programming language from TypeScript, JavaScript, Python, or Rust. The code panel below updates to show the equivalent greet function in the chosen language.
function greet(name: string): string {
return `Hello, ${name}!`;
}// biome-ignore-all lint/suspicious/noTemplateCurlyInString:<>
"use client";
import { useState } from "react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
const languages = [
{ ext: ".ts", id: "typescript", label: "TypeScript" },
{ ext: ".js", id: "javascript", label: "JavaScript" },
{ ext: ".py", id: "python", label: "Python" },
{ ext: ".rs", id: "rust", label: "Rust" },
];
const snippets: Record<string, string> = {
javascript: "function greet(name) {\n return `Hello, ${name}!`;\n}",
python: `def greet(name: str) -> str:\n return f"Hello, {name}!"`,
rust: `fn greet(name: &str) -> String {\n format!("Hello, {}!", name)\n}`,
typescript:
"function greet(name: string): string {\n return `Hello, ${name}!`;\n}",
};
export function Pattern() {
const [lang, setLang] = useState("typescript");
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<ToggleGroup
onValueChange={(v) => {
const next = v[v.length - 1];
if (next) setLang(next);
}}
value={[lang]}
variant="outline"
>
{languages.map((l) => (
<ToggleGroupItem
aria-label={`Select ${l.label}`}
className="text-xs"
key={l.id}
value={l.id}
>
{l.label}
</ToggleGroupItem>
))}
</ToggleGroup>
<pre className="overflow-x-auto rounded-lg border border-input bg-muted/20 p-3 font-mono text-foreground/80 text-xs leading-relaxed">
{snippets[lang]}
</pre>
</div>
);
}
Column Sort Direction
A per-row ascending / descending sort toggle for each table column. Each row shows a toggle group with Up and Down arrow items; clearing the selection resets that column.
Click a column header to cycle sort direction
"use client";
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon } from "lucide-react";
import { useState } from "react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
type SortDir = "asc" | "desc" | "none";
const columns = ["Name", "Status", "Date", "Amount"];
export function Pattern() {
const [sorts, setSorts] = useState<Record<string, SortDir>>({
Amount: "none",
Date: "asc",
Name: "none",
Status: "none",
});
const _sortIcon = (dir: SortDir) => {
if (dir === "asc") return <ArrowUpIcon className="size-3.5" />;
if (dir === "desc") return <ArrowDownIcon className="size-3.5" />;
return <ArrowUpDownIcon className="size-3.5 opacity-40" />;
};
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<p className="text-muted-foreground text-xs">
Click a column header to cycle sort direction
</p>
{columns.map((col) => {
const dir = sorts[col];
return (
<div
className="flex items-center justify-between rounded-lg border border-input px-3 py-2"
key={col}
>
<span className="text-sm">{col}</span>
<ToggleGroup
onValueChange={(v) => {
const next: SortDir = (v[0] as SortDir) ?? "none";
setSorts((prev) => ({ ...prev, [col]: next }));
}}
value={dir && dir !== "none" ? [dir] : []}
>
<ToggleGroupItem
aria-label={`Sort ${col} ascending`}
size="sm"
value="asc"
>
<ArrowUpIcon className="size-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
aria-label={`Sort ${col} descending`}
size="sm"
value="desc"
>
<ArrowDownIcon className="size-3.5" />
</ToggleGroupItem>
</ToggleGroup>
</div>
);
})}
</div>
);
}
Issue Priority Picker
Each issue card has an inline priority group (Low / Medium / High / Critical) with a color-coded dot and label. Selecting a priority updates the badge in the card header live.
Login page unresponsive on mobile
Dashboard charts not loading
Typo in onboarding copy
// biome-ignore-all lint/style/noNonNullAssertion:<>
"use client";
import { useState } from "react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
type Priority = "low" | "medium" | "high" | "critical";
const priorities: {
color: string;
dot: string;
id: Priority;
label: string;
}[] = [
{
color: "text-muted-foreground",
dot: "bg-muted-foreground/60",
id: "low",
label: "Low",
},
{ color: "text-blue-500", dot: "bg-blue-500", id: "medium", label: "Medium" },
{ color: "text-amber-500", dot: "bg-amber-500", id: "high", label: "High" },
{
color: "text-red-500",
dot: "bg-red-500",
id: "critical",
label: "Critical",
},
];
const issues = [
{ id: "ISS-101", title: "Login page unresponsive on mobile" },
{ id: "ISS-102", title: "Dashboard charts not loading" },
{ id: "ISS-103", title: "Typo in onboarding copy" },
];
export function Pattern() {
const [issuePriorities, setIssuePriorities] = useState<
Record<string, Priority>
>({
"ISS-101": "critical",
"ISS-102": "high",
"ISS-103": "low",
});
return (
<div className="flex w-full max-w-sm flex-col gap-3">
{issues.map((issue) => {
const current = issuePriorities[issue.id] ?? "low";
const p = priorities.find((x) => x.id === current)!;
return (
<div
className="flex flex-col gap-2 rounded-lg border border-border p-3"
key={issue.id}
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-muted-foreground text-xs">
{issue.id}
</span>
<div className="flex items-center gap-1.5">
<span className={`size-2 rounded-full ${p.dot}`} />
<span className={`font-medium text-xs ${p.color}`}>
{p.label}
</span>
</div>
</div>
<p className="text-sm leading-snug">{issue.title}</p>
<ToggleGroup
className="justify-start"
onValueChange={(v) => {
if (v[0])
setIssuePriorities((prev) => ({
...prev,
[issue.id]: v[0] as Priority,
}));
}}
size="sm"
value={[current]}
variant="outline"
>
{priorities.map((pri) => (
<ToggleGroupItem
aria-label={`Set ${pri.label} priority`}
className={`gap-1 text-xs ${issuePriorities[issue.id] === pri.id ? pri.color : ""}`}
key={pri.id}
value={pri.id}
>
<span className={`size-1.5 rounded-full ${pri.dot}`} />
{pri.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
);
})}
</div>
);
}
Date Range Quick-Select
Five quick-select range options — Today, Week, Month, Quarter, Year — each showing a sublabel date range. Switching updates a three-column stats grid below with the relevant metrics.
"use client";
import { useState } from "react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
type Range = "today" | "week" | "month" | "quarter" | "year";
const ranges: { id: Range; label: string; sublabel: string }[] = [
{ id: "today", label: "Today", sublabel: "Jun 3" },
{ id: "week", label: "Week", sublabel: "Jun 1–7" },
{ id: "month", label: "Month", sublabel: "June" },
{ id: "quarter", label: "Quarter", sublabel: "Q2" },
{ id: "year", label: "Year", sublabel: "2025" },
];
const stats: Record<Range, { label: string; value: string }[]> = {
month: [
{ label: "Revenue", value: "$34,892" },
{ label: "Sessions", value: "128,430" },
{ label: "Conversions", value: "3.4%" },
],
quarter: [
{ label: "Revenue", value: "$98,241" },
{ label: "Sessions", value: "385,291" },
{ label: "Conversions", value: "3.1%" },
],
today: [
{ label: "Revenue", value: "$1,284" },
{ label: "Sessions", value: "4,231" },
{ label: "Conversions", value: "2.8%" },
],
week: [
{ label: "Revenue", value: "$8,942" },
{ label: "Sessions", value: "29,840" },
{ label: "Conversions", value: "3.2%" },
],
year: [
{ label: "Revenue", value: "$412,840" },
{ label: "Sessions", value: "1.54M" },
{ label: "Conversions", value: "3.3%" },
],
};
export function Pattern() {
const [range, setRange] = useState<Range>("month");
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<ToggleGroup
className="w-full"
onValueChange={(v) => v[0] && setRange(v[0] as Range)}
value={[range]}
variant="outline"
>
{ranges.map((r) => (
<ToggleGroupItem
aria-label={`View ${r.label} stats`}
className="flex-1 flex-col gap-0 py-2 text-xs leading-none"
key={r.id}
value={r.id}
>
<span className="font-medium">{r.label}</span>
<span className="mt-0.5 text-[10px] text-muted-foreground">
{r.sublabel}
</span>
</ToggleGroupItem>
))}
</ToggleGroup>
<div className="grid grid-cols-3 gap-2">
{stats[range].map((s) => (
<div
className="flex flex-col gap-0.5 rounded-lg border border-border p-3 text-center"
key={s.label}
>
<span className="font-bold text-lg">{s.value}</span>
<span className="text-muted-foreground text-xs">{s.label}</span>
</div>
))}
</div>
</div>
);
}
Code Viewer Toolbar
Multiple-select toggle group for editor options: Word wrap, Case sensitive, Filter, and Sort. Active options are listed below the viewer panel and update as toggles are pressed.
Word wrap on ·
"use client";
import {
CaseSensitiveIcon,
FilterIcon,
SortAscIcon,
WrapTextIcon,
} from "lucide-react";
import { useState } from "react";
import { Separator } from "@/components/ui/separator";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
export function Pattern() {
const [activeOptions, setActiveOptions] = useState<string[]>(["wrap"]);
const isActive = (id: string) => activeOptions.includes(id);
return (
<div className="flex w-full max-w-xs flex-col gap-4">
<div className="rounded-lg border border-border">
<div className="flex items-center gap-1 border-border border-b bg-muted/30 px-2 py-1">
<ToggleGroup
className="gap-0"
onValueChange={setActiveOptions}
value={activeOptions}
>
<ToggleGroupItem
aria-label="Toggle word wrap"
size="sm"
value="wrap"
>
<WrapTextIcon className="size-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
aria-label="Toggle case-sensitive search"
size="sm"
value="case"
>
<CaseSensitiveIcon className="size-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
aria-label="Toggle filters"
size="sm"
value="filter"
>
<FilterIcon className="size-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
aria-label="Toggle sort ascending"
size="sm"
value="sort"
>
<SortAscIcon className="size-3.5" />
</ToggleGroupItem>
</ToggleGroup>
<Separator className="mx-1 h-4" orientation="vertical" />
<span className="text-muted-foreground text-xs">
{activeOptions.length} active
</span>
</div>
<div className="space-y-1.5 p-3">
{[
"src/components/Button.tsx",
"src/components/Input.tsx",
"src/components/Badge.tsx",
"src/hooks/useForm.ts",
].map((file) => (
<div
className="flex items-center gap-2 text-muted-foreground text-xs"
key={file}
>
<span className="size-1.5 rounded-full bg-emerald-500" />
{file}
</div>
))}
</div>
</div>
<p className="text-center text-muted-foreground text-xs">
{isActive("wrap") && "Word wrap on · "}
{isActive("case") && "Case sensitive · "}
{isActive("filter") && "Filtered · "}
{isActive("sort") && "Sorted asc"}
{activeOptions.length === 0 && "No options active"}
</p>
</div>
);
}
Zoom Level
A five-step zoom group (50 % – 150 %) that scales a preview card via CSS transform: scale(). The current zoom percentage is displayed below the preview.
Hello, world!
Zoom: 100%
Current zoom: 100%
"use client";
import { useState } from "react";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group";
type Zoom = "50" | "75" | "100" | "125" | "150";
const levels: Zoom[] = ["50", "75", "100", "125", "150"];
export function Pattern() {
const [zoom, setZoom] = useState<Zoom>("100");
const scale = Number(zoom) / 100;
return (
<div className="flex w-full max-w-xs flex-col items-center gap-5">
<ToggleGroup
onValueChange={(v) => v[0] && setZoom(v[0] as Zoom)}
size="sm"
value={[zoom]}
variant="outline"
>
{levels.map((z) => (
<ToggleGroupItem
aria-label={`Zoom to ${z}%`}
className="min-w-10 text-xs tabular-nums"
key={z}
value={z}
>
{z}%
</ToggleGroupItem>
))}
</ToggleGroup>
<div
className="flex h-32 w-full items-center justify-center overflow-hidden rounded-lg border border-border bg-muted/20"
style={{ perspective: "400px" }}
>
<div
className="rounded-md border border-border bg-background p-4 shadow-sm transition-transform duration-300"
style={{ transform: `scale(${scale})` }}
>
<p className="font-semibold text-sm">Hello, world!</p>
<p className="mt-0.5 text-muted-foreground text-xs">Zoom: {zoom}%</p>
</div>
</div>
<p className="text-muted-foreground text-xs">
Current zoom:{" "}
<span className="font-medium text-foreground">{zoom}%</span>
</p>
</div>
);
}

