Toggle Group
A row of toggles with single or multiple selection - items inherit variant and size from the group, and the outline variant becomes a segmented control.
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
export default function ToggleGroupDemo() {
return (
<div className="flex flex-col items-center gap-3">
<ToggleGroup type="single" defaultValue="left" variant="outline">
<ToggleGroupItem value="left" aria-label="Align left">
<AlignLeft />
</ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center">
<AlignCenter />
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right">
<AlignRight />
</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup type="single" defaultValue="left" variant="outline" tone="brand">
<ToggleGroupItem value="left" aria-label="Align left">
<AlignLeft />
</ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center">
<AlignCenter />
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right">
<AlignRight />
</ToggleGroupItem>
</ToggleGroup>
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/toggle-groupDependencies, the @ai2/tokens theme and the component file are installed together. The @ai2/toggle component installs alongside - the group reuses its variants.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/toggle-group.tsx"use client"
import * as React from "react"
import { ToggleGroup as ToggleGroupPrimitive } from "@/components/ui/primitives"
import type { VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
variant: "default",
size: "md",
tone: "neutral",
})
function ToggleGroup({
className,
variant,
size,
tone,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
/* WCAG 1.4.4: the group is `w-fit`, so at 200% text it widened the page.
Scrolling was moved INSIDE the group. The wrapper's rationale and the
`-m-1 p-1` pair are documented in detail in tabs.tsx: `overflow-x-auto`
clips vertically too, which is why the focus ring (3px) and the outline
variant's shadow need room. */
return (
<div
data-slot="toggle-group-viewport"
className="-m-1 max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant ?? "default"}
className={cn(
"group/toggle-group flex w-fit items-center gap-0.5 rounded-lg data-[variant=outline]:gap-0 data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, tone }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
</div>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
tone,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const ctx = React.useContext(ToggleGroupContext)
const v = variant ?? ctx.variant
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-tone={tone ?? ctx.tone ?? "neutral"}
className={cn(
toggleVariants({ variant: v, size: size ?? ctx.size, tone: tone ?? ctx.tone }),
v === "outline" &&
"rounded-none border-l-0 first:rounded-l-lg first:border-l last:rounded-r-lg",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }The file imports toggleVariants from Toggle - copy that component too. Manual installs also skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
<ToggleGroup type="single" defaultValue="left" variant="outline">
<ToggleGroupItem value="left" aria-label="Align left">
<AlignLeft />
</ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center">
<AlignCenter />
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right">
<AlignRight />
</ToggleGroupItem>
</ToggleGroup>type is required: "single" allows one pressed item at a time, "multiple" allows any number. Items inherit the group's variant, size and tone but can override them individually.
Examples
Multiple selection
Outline segmented control
With variant="outline" the items lose their gap, share borders and round only the outer corners.
Variants
variant="default" keeps the items as loose transparent buttons; variant="outline" fuses them into a segmented control.
Sizes
Tones
tone colors the pressed items: neutral uses the accent token, brand the brand-soft token. Every item inherits it through context.
Disabled
disabled on the root locks the whole group; on a single item it grays out just that segment.
Props
ToggleGroupItem accepts the same three props to override the group. Both parts also accept their radix props - type, value, defaultValue and onValueChange included.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "outline" | "default" | Visual style shared with items via context. Outline joins the items into a segmented control. |
size | "sm" | "md" | "lg" | "md" | Item height, minimum width and padding, shared via context. |
tone | "neutral" | "brand" | "neutral" | Pressed-state color shared via context: neutral maps to the accent token, brand to the brand-soft token. |
ai2 Toggle Group: single or multiple selection toggles for React
The ai2 Toggle Group is a shadcn-compatible toggle group component for React, built on the radix-ui ToggleGroup primitive and styled with Tailwind CSS v4. It turns a row of toggle buttons into one coherent control: single selection for choices like text alignment or a code/preview switch, multiple selection for independent flags like bold, italic and underline.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Toggle Group?
It is a two-part composition: ToggleGroup and ToggleGroupItem. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working without changes. The required type prop picks the selection model: single allows one pressed item at a time, multiple allows any number.
The group does not define its own item styles. It imports toggleVariants from the ai2 Toggle and passes variant, size and tone to every item through React context, so standalone toggles and grouped toggles are pixel-identical. With the outline variant the items fuse into a segmented control: shared borders, no gaps, rounded outer corners only.
Why use it
- Accessible by construction: The radix-ui ToggleGroup primitive manages the group semantics and each item's aria-pressed or selection state, so screen readers announce what is selected without extra wiring.
- One prop for the selection model: type="single" turns the row into an exclusive choice, type="multiple" into independent flags. The pressed logic, including deselection, is handled for you.
- Keyboard navigation across the set: Roving focus lets arrow keys move between items while Enter and Space toggle the focused one, so the whole group costs one tab stop.
- Shared styles with Toggle: Items render through the same toggleVariants cva as the standalone Toggle (2 variants, 3 sizes, 2 tones), so mixed toolbars stay visually consistent by construction.
- Segmented control for free: variant="outline" collapses gaps and merges borders so the group reads as one segmented control, a pattern you would otherwise hand-build.
Features
- shadcn registry install: One command adds the component together with @ai2/toggle, whose variants it reuses, plus the @ai2/tokens theme.
- Context-based variant, size and tone: Set variant, size and tone once on the group; every item inherits them through React context and can still override individually.
- Controlled and uncontrolled state: Use defaultValue for simple cases or drive value and onValueChange from React state; single mode uses a string, multiple mode a string array.
- Outline segmented rendering: Outline items drop their inner rounding and left borders, keeping a single border between segments and rounded corners only at the ends.
- Token-driven pressed state: Pressed items use the accent background and foreground from the token file, or the brand-soft tokens with tone="brand", so selection follows your theme in light and dark mode.
- Data attributes for styling: The root exposes data-slot="toggle-group" and data-variant; items expose data-slot="toggle-group-item" and data-state, so CSS overrides stay targeted.
Production tips
- Label icon-only items: Each icon-only ToggleGroupItem needs its own aria-label, like aria-label="Align left", so screen readers can name every option in the set.
- Pick single vs multiple by meaning: Mutually exclusive choices (alignment, view mode) are type="single"; independent flags (bold, italic, underline) are type="multiple". Do not use single with flags just for the look.
- Watch for empty single selection: In single mode, clicking the pressed item deselects it and onValueChange fires with an empty string. If your UI needs a value at all times, ignore empty updates in the handler.
- Use outline for segmented controls: The outline variant reads as one control and suits code/preview/split style switches; the default variant reads as loose toolbar buttons.
- Keep groups short: A toggle group scans well up to around five items. For longer option lists, a Select or a Tabs row communicates the choice better.
Works with the rest of ai2
The group is built from the ai2 Toggle and shares its exact variants. In toolbars, divide clusters with an ai2 Separator and explain icon-only items with an ai2 Tooltip.
When the choice reveals a panel instead of changing a setting, use ai2 Tabs; when options overflow a row, move them into an ai2 Select. Everything shares one token source, so combinations stay visually consistent in both modes.