Toggle
A two-state button that can be on or off. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
<Toggle aria-label="Toggle bold" defaultPressed>
<BoldIcon />
</Toggle>
<Toggle aria-label="Toggle italic">
<ItalicIcon />
</Toggle>
<Toggle aria-label="Toggle underline">
<UnderlineIcon />
</Toggle>
</div>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/toggle
Usage
import { Toggle } from "@/components/ui/toggle"<Toggle>Toggle</Toggle>Examples
Outline Variant
The outline variant renders toggles with a visible border, making the pressed/unpressed distinction clearer against light backgrounds.
import { BoldIcon, ItalicIcon } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
<Toggle aria-label="Toggle italic" variant="outline">
<ItalicIcon />
Italic
</Toggle>
<Toggle aria-label="Toggle bold" variant="outline">
<BoldIcon />
Bold
</Toggle>
</div>
);
}
Sizes
Renders the three size variants — sm, default, and lg — for layout comparison and scale matching.
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
<Toggle aria-label="Small toggle" size="sm" variant="outline">
Small
</Toggle>
<Toggle aria-label="Default toggle" size="default" variant="outline">
Default
</Toggle>
<Toggle aria-label="Large toggle" size="lg" variant="outline">
Large
</Toggle>
</div>
);
}
With Label and Icon
Combines a text label and a leading icon inside a single toggle, a common pattern for formatting toolbars.
import { BoldIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
return (
<div className="flex flex-col items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Button size="sm" variant="outline">
Button
</Button>
<Toggle aria-label="Small toggle" size="sm" variant="outline">
Toggle
</Toggle>
</div>
<div className="flex items-center gap-2">
<Button size="icon" variant="outline">
<BoldIcon />
</Button>
<Toggle aria-label="Toggle bold icon" variant="outline">
<BoldIcon />
</Toggle>
</div>
</div>
);
}
Toggle with icon
An icon-only toggle. Always add aria-label so screen readers can identify the action.
import { BookmarkIcon } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Toggle aria-label="Toggle bookmark" variant="outline">
<BookmarkIcon className="group-data-[state=on]/toggle:fill-accent-foreground" />
Bookmark
</Toggle>
</div>
);
}
With Count Badge
A toggle that carries a live badge count — useful for inbox or filter toggles where the number of affected items matters.
"use client";
import { BellIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
const [pressed, setPressed] = useState(false);
return (
<div className="flex items-center justify-center">
<Toggle
aria-label="Toggle notifications"
onPressedChange={setPressed}
pressed={pressed}
>
<div className="relative">
<BellIcon />
{!pressed && (
<Badge
className="absolute -top-3 -right-3 h-fit rounded-full!"
size="sm"
variant="destructive"
>
3
</Badge>
)}
</div>
</Toggle>
</div>
);
}
Icon Swap
The icon changes between two states on press, giving immediate visual confirmation that the toggle is active.
"use client";
import { HeartIcon } from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
const [pressed, setPressed] = useState(false);
return (
<div className="flex items-center justify-center">
<Toggle
aria-label="Toggle favorite"
onPressedChange={setPressed}
pressed={pressed}
>
{pressed ? <HeartIcon className="fill-current" /> : <HeartIcon />}
</Toggle>
</div>
);
}
Dynamic Label
The label text updates when the toggle is pressed, making the current state explicit instead of relying on styling alone.
"use client";
import { BookmarkCheckIcon, BookmarkIcon } from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
const [pressed, setPressed] = useState(false);
return (
<div className="flex items-center justify-center">
<Toggle
aria-label="Toggle bookmark"
onPressedChange={setPressed}
pressed={pressed}
variant="outline"
>
{pressed ? (
<BookmarkCheckIcon className="fill-current" />
) : (
<BookmarkIcon />
)}
{pressed ? "Bookmarked" : "Bookmark"}
</Toggle>
</div>
);
}
Video Call Controls
A call overlay card with an animated status indicator. Mic, camera, and screen-share toggles swap icons and turn destructive red when disabled. A static Leave button ends the call.
"use client";
import {
MicIcon,
MicOffIcon,
MonitorIcon,
MonitorOffIcon,
PhoneOffIcon,
VideoIcon,
VideoOffIcon,
} from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
export function Pattern() {
const [micOn, setMicOn] = useState(true);
const [camOn, setCamOn] = useState(true);
const [screenOn, setScreenOn] = useState(false);
return (
<div className="flex flex-col items-center gap-6">
<div className="flex w-full max-w-xs flex-col items-center gap-2 rounded-2xl border bg-card px-6 py-5 shadow-sm">
<div className="mb-1 flex items-center gap-1.5">
<span className="relative flex size-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-green-500" />
</span>
<span className="text-muted-foreground text-xs">In call · 12:34</span>
</div>
<div className="flex items-end gap-3">
<div className="flex flex-col items-center gap-1.5">
<Toggle
aria-label="Toggle microphone"
className={
!micOn
? "border-destructive/20 bg-destructive/10 text-destructive hover:bg-destructive/15 data-pressed:bg-destructive/10"
: ""
}
onPressedChange={(p) => setMicOn(!p)}
pressed={!micOn}
size="lg"
variant="outline"
>
{micOn ? <MicIcon /> : <MicOffIcon />}
</Toggle>
<span className="text-muted-foreground text-xs">
{micOn ? "Mute" : "Unmute"}
</span>
</div>
<div className="flex flex-col items-center gap-1.5">
<Toggle
aria-label="Toggle camera"
className={
!camOn
? "border-destructive/20 bg-destructive/10 text-destructive hover:bg-destructive/15 data-pressed:bg-destructive/10"
: ""
}
onPressedChange={(p) => setCamOn(!p)}
pressed={!camOn}
size="lg"
variant="outline"
>
{camOn ? <VideoIcon /> : <VideoOffIcon />}
</Toggle>
<span className="text-muted-foreground text-xs">
{camOn ? "Stop video" : "Start video"}
</span>
</div>
<div className="flex flex-col items-center gap-1.5">
<Toggle
aria-label="Toggle screen share"
onPressedChange={setScreenOn}
pressed={screenOn}
size="lg"
variant="outline"
>
{screenOn ? <MonitorOffIcon /> : <MonitorIcon />}
</Toggle>
<span className="text-muted-foreground text-xs">
{screenOn ? "Stop share" : "Share screen"}
</span>
</div>
<div className="flex flex-col items-center gap-1.5">
<button
aria-label="Leave call"
className="inline-flex size-10 items-center justify-center rounded-lg bg-destructive text-white shadow-xs transition-opacity hover:opacity-90 sm:size-9"
type="button"
>
<PhoneOffIcon className="size-4" />
</button>
<span className="text-muted-foreground text-xs">Leave</span>
</div>
</div>
</div>
</div>
);
}
Role Filter Chips
Pill-style toggle chips with per-category badge counts. The total result count updates reactively and a "Clear" link appears only when at least one filter is active.
"use client";
import { SlidersHorizontalIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Toggle } from "@/components/ui/toggle";
const filters = [
{ count: 24, id: "design", label: "Design" },
{ count: 41, id: "engineering", label: "Engineering" },
{ count: 18, id: "product", label: "Product" },
{ count: 12, id: "marketing", label: "Marketing" },
{ count: 7, id: "leadership", label: "Leadership" },
{ count: 9, id: "research", label: "Research" },
];
export function Pattern() {
const [active, setActive] = useState<Set<string>>(new Set());
const toggle = (id: string) =>
setActive((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
const totalResults =
active.size === 0
? filters.reduce((sum, f) => sum + f.count, 0)
: filters
.filter((f) => active.has(f.id))
.reduce((sum, f) => sum + f.count, 0);
return (
<div className="w-full max-w-sm space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-muted-foreground text-sm">
<SlidersHorizontalIcon className="size-3.5" />
<span>Filter by role</span>
</div>
<div className="flex items-center gap-1.5">
<span className="text-muted-foreground text-xs">
{totalResults} results
</span>
{active.size > 0 && (
<button
className="text-muted-foreground text-xs underline-offset-2 hover:text-foreground hover:underline"
onClick={() => setActive(new Set())}
type="button"
>
Clear
</button>
)}
</div>
</div>
<div className="flex flex-wrap gap-2">
{filters.map((filter) => {
const isActive = active.has(filter.id);
return (
<Toggle
aria-label={`Filter by ${filter.label}`}
className="gap-1.5 rounded-full"
key={filter.id}
onPressedChange={() => toggle(filter.id)}
pressed={isActive}
size="sm"
variant="outline"
>
{filter.label}
<Badge
className={`transition-colors ${isActive ? "bg-primary-foreground/20 text-current" : ""}`}
size="sm"
variant={isActive ? "default" : "outline"}
>
{filter.count}
</Badge>
</Toggle>
);
})}
</div>
</div>
);
}
Bookmark Toggle
Each article row has a bookmark toggle that fills the icon when pressed. A counter below tracks how many articles are saved across the list.
0 of 3 saved
"use client";
import { BookmarkIcon } from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
const articles = [
{
author: "Olivia Martin",
id: "1",
title: "Building scalable design systems with Base UI",
},
{
author: "Jackson Lee",
id: "2",
title: "Tailwind CSS v4 migration guide",
},
{
author: "Isabella Nguyen",
id: "3",
title: "Accessible React components from scratch",
},
];
export function Pattern() {
const [saved, setSaved] = useState<Set<string>>(new Set());
const toggle = (id: string) =>
setSaved((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
return (
<div className="w-full max-w-sm space-y-2">
{articles.map((a) => {
const isBookmarked = saved.has(a.id);
return (
<div
className="flex items-center justify-between gap-2 rounded-lg border border-border px-3 py-2.5"
key={a.id}
>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="truncate font-medium text-sm">{a.title}</span>
<span className="text-muted-foreground text-xs">{a.author}</span>
</div>
<Toggle
aria-label={isBookmarked ? "Remove bookmark" : "Bookmark article"}
className="shrink-0"
onPressedChange={() => toggle(a.id)}
pressed={isBookmarked}
size="sm"
variant="outline"
>
<BookmarkIcon
className={`size-4 transition-colors ${isBookmarked ? "fill-current" : ""}`}
/>
</Toggle>
</div>
);
})}
<p className="text-center text-muted-foreground text-xs">
{saved.size} of {articles.length} saved
</p>
</div>
);
}
Rich-Text Formatting Toolbar
Strikethrough, inline code, blockquote, and link toggles each apply markdown syntax to a live preview string below the toolbar.
No formatting applied
"use client";
import { CodeIcon, LinkIcon, QuoteIcon, StrikethroughIcon } from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
const tools = [
{ icon: StrikethroughIcon, id: "strike", label: "Strikethrough" },
{ icon: CodeIcon, id: "code", label: "Inline code" },
{ icon: QuoteIcon, id: "quote", label: "Blockquote" },
{ icon: LinkIcon, id: "link", label: "Link" },
];
export function Pattern() {
const [active, setActive] = useState<Set<string>>(new Set());
const toggle = (id: string) =>
setActive((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
const preview = () => {
let text = "The quick brown fox";
if (active.has("strike")) text = `~~${text}~~`;
if (active.has("code")) text = `\`${text}\``;
if (active.has("quote")) text = `> ${text}`;
if (active.has("link")) text = `[${text}](https://example.com)`;
return text || "The quick brown fox";
};
return (
<div className="w-full max-w-xs space-y-3">
<div className="flex items-center gap-1 rounded-lg border border-input bg-muted/30 p-1">
{tools.map(({ icon: Icon, id, label }) => (
<Toggle
aria-label={label}
key={id}
onPressedChange={() => toggle(id)}
pressed={active.has(id)}
size="sm"
>
<Icon className="size-3.5" />
</Toggle>
))}
</div>
<div className="rounded-lg border border-input bg-background px-3 py-2 font-mono text-foreground/80 text-sm">
{preview()}
</div>
<p className="text-center text-muted-foreground text-xs">
{active.size > 0
? `Active: ${[...active].join(", ")}`
: "No formatting applied"}
</p>
</div>
);
}
Social Post Interactions
Reply, repost, like, and share toggles on a post card. The repost and like counts increment live when pressed and the heart fills with a color transition.
Jackson Lee
@jacksonlee · 2h
Just shipped the new component library update. 15 new variants, dark mode support, and zero breaking changes. 🚀
"use client";
import {
HeartIcon,
MessageCircleIcon,
RepeatIcon,
Share2Icon,
} from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
const initialCounts = { hearts: 142, replies: 24, reposts: 38 };
export function Pattern() {
const [liked, setLiked] = useState(false);
const [reposted, setReposted] = useState(false);
return (
<div className="w-full max-w-xs rounded-xl border border-border p-4">
<div className="mb-3 flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-full bg-muted font-medium text-xs">
JL
</div>
<div>
<p className="font-medium text-sm leading-none">Jackson Lee</p>
<p className="text-muted-foreground text-xs">@jacksonlee · 2h</p>
</div>
</div>
<p className="mb-4 text-sm leading-relaxed">
Just shipped the new component library update. 15 new variants, dark
mode support, and zero breaking changes. 🚀
</p>
<div className="flex items-center gap-1">
<Toggle
aria-label="Reply"
className="gap-1.5 text-muted-foreground"
size="sm"
>
<MessageCircleIcon className="size-4" />
<span className="text-xs tabular-nums">{initialCounts.replies}</span>
</Toggle>
<Toggle
aria-label={reposted ? "Undo repost" : "Repost"}
className={`gap-1.5 transition-colors ${reposted ? "text-emerald-500" : "text-muted-foreground"}`}
onPressedChange={setReposted}
pressed={reposted}
size="sm"
>
<RepeatIcon className="size-4" />
<span className="text-xs tabular-nums">
{initialCounts.reposts + (reposted ? 1 : 0)}
</span>
</Toggle>
<Toggle
aria-label={liked ? "Unlike" : "Like"}
className={`gap-1.5 transition-colors ${liked ? "text-rose-500" : "text-muted-foreground"}`}
onPressedChange={setLiked}
pressed={liked}
size="sm"
>
<HeartIcon
className={`size-4 transition-colors ${liked ? "fill-rose-500" : ""}`}
/>
<span className="text-xs tabular-nums">
{initialCounts.hearts + (liked ? 1 : 0)}
</span>
</Toggle>
<Toggle
aria-label="Share"
className="ml-auto text-muted-foreground"
size="sm"
>
<Share2Icon className="size-4" />
</Toggle>
</div>
</div>
);
}
Server Settings
A settings card with icon-swap toggles for Secure mode, Network access, and Debug visibility. Active toggles turn green; inactive ones show the off-state icon in muted color.
Server settings
"use client";
import {
EyeIcon,
EyeOffIcon,
LockIcon,
UnlockIcon,
WifiIcon,
WifiOffIcon,
} from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
const settings = [
{
description: "Allow only HTTPS connections",
id: "secure",
initial: true,
offIcon: UnlockIcon,
offLabel: "Insecure",
onIcon: LockIcon,
onLabel: "Secure mode",
},
{
description: "Restrict access to public networks",
id: "network",
initial: true,
offIcon: WifiOffIcon,
offLabel: "Offline",
onIcon: WifiIcon,
onLabel: "Network access",
},
{
description: "Show stack traces in error responses",
id: "debug",
initial: false,
offIcon: EyeOffIcon,
offLabel: "Debug hidden",
onIcon: EyeIcon,
onLabel: "Debug visible",
},
];
export function Pattern() {
const [states, setStates] = useState<Record<string, boolean>>(
Object.fromEntries(settings.map((s) => [s.id, s.initial])),
);
const toggle = (id: string) =>
setStates((prev) => ({ ...prev, [id]: !prev[id] }));
return (
<div className="w-full max-w-xs space-y-2">
<p className="mb-3 font-semibold text-sm">Server settings</p>
{settings.map((s) => {
const on = states[s.id];
const Icon = on ? s.onIcon : s.offIcon;
return (
<div
className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2.5"
key={s.id}
>
<div className="flex flex-col gap-0.5">
<span className="font-medium text-sm">
{on ? s.onLabel : s.offLabel}
</span>
<span className="text-muted-foreground text-xs">
{s.description}
</span>
</div>
<Toggle
aria-label={on ? s.offLabel : s.onLabel}
className={on ? "text-emerald-500" : "text-muted-foreground"}
onPressedChange={() => toggle(s.id)}
pressed={on}
size="sm"
variant="outline"
>
<Icon className="size-4" />
</Toggle>
</div>
);
})}
</div>
);
}
Task Priority Flags
A task list where each item has a row of flag toggles for Low / Medium / High / Urgent priority. A separate checkmark toggle marks tasks as done and applies a strikethrough.
Task priority
"use client";
import { CalendarIcon, CheckIcon, FlagIcon } from "lucide-react";
import { useState } from "react";
import { Toggle } from "@/components/ui/toggle";
type Priority = "low" | "medium" | "high" | "urgent";
const priorities: {
color: string;
id: Priority;
label: string;
}[] = [
{ color: "text-muted-foreground", id: "low", label: "Low" },
{ color: "text-blue-500", id: "medium", label: "Medium" },
{ color: "text-amber-500", id: "high", label: "High" },
{ color: "text-red-500", id: "urgent", label: "Urgent" },
];
const tasks = [
{ due: "Today", id: "t1", status: "open", title: "Review PR #482" },
{ due: "Tomorrow", id: "t2", status: "open", title: "Update API docs" },
{ due: "Jun 10", id: "t3", status: "open", title: "Write release notes" },
];
export function Pattern() {
const [taskPriorities, setTaskPriorities] = useState<
Record<string, Priority>
>({ t1: "high", t2: "medium", t3: "low" });
const [done, setDone] = useState<Set<string>>(new Set());
return (
<div className="w-full max-w-sm space-y-3">
<p className="font-semibold text-sm">Task priority</p>
{tasks.map((task) => (
<div
className={`rounded-lg border border-border p-3 transition-opacity ${done.has(task.id) ? "opacity-50" : ""}`}
key={task.id}
>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Toggle
aria-label={
done.has(task.id) ? "Mark incomplete" : "Mark complete"
}
className={
done.has(task.id)
? "text-emerald-500"
: "text-muted-foreground"
}
onPressedChange={() =>
setDone((prev) => {
const next = new Set(prev);
next.has(task.id)
? next.delete(task.id)
: next.add(task.id);
return next;
})
}
pressed={done.has(task.id)}
size="sm"
>
<CheckIcon className="size-3.5" />
</Toggle>
<span
className={`font-medium text-sm ${done.has(task.id) ? "text-muted-foreground line-through" : ""}`}
>
{task.title}
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground text-xs">
<CalendarIcon className="size-3" />
{task.due}
</div>
</div>
<div className="flex gap-1">
{priorities.map((p) => (
<Toggle
aria-label={`Set ${p.label} priority`}
className={`gap-1 text-xs ${taskPriorities[task.id] === p.id ? p.color : "text-muted-foreground/60"}`}
key={p.id}
onPressedChange={() =>
setTaskPriorities((prev) => ({ ...prev, [task.id]: p.id }))
}
pressed={taskPriorities[task.id] === p.id}
size="sm"
>
<FlagIcon className="size-3" />
{p.label}
</Toggle>
))}
</div>
</div>
))}
</div>
);
}

