Button Group
A segmented control that fuses buttons and inputs into one unit - horizontal or vertical, with optional text and separator parts.
import { Bold, Italic, Underline } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@/components/ui/button-group"
export default function ButtonGroupDemo() {
return (
<div className="flex flex-col gap-4">
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Bold">
<Bold />
</Button>
<Button variant="outline" size="icon" aria-label="Italic">
<Italic />
</Button>
<Button variant="outline" size="icon" aria-label="Underline">
<Underline />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Prev</Button>
<ButtonGroupSeparator />
<Button variant="outline">Next</Button>
</ButtonGroup>
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/button-groupDependencies, the @ai2/button and @ai2/separator components, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Button Group renders through the ai2 Button and Separator, so add those components first.
Add 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/button-group.tsximport type * as React from "react"
import { Slot } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation ?? "horizontal"}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
data-slot="button-group-text"
className={cn(
"flex items-center gap-2 rounded-lg border border-field-border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_i]:pointer-events-none [&_i]:text-base [&_i]:leading-none",
className
)}
{...props}
/>
)
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"relative m-0 self-stretch bg-input data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the surface colors will be missing.
Usage
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
<ButtonGroup>
<Button variant="outline">Day</Button>
<Button variant="outline">Week</Button>
<Button variant="outline">Month</Button>
</ButtonGroup>Wrap ai2 Buttons in a ButtonGroup to fuse them into one segmented control. Give every child the same variant and size so the merged edges line up.
Examples
Horizontal
The default orientation merges the left and right edges of adjacent buttons into one segmented row.
Vertical
Set orientation="vertical" to stack the buttons and merge their top and bottom edges instead.
With separator
ButtonGroupSeparator draws a token-colored divider between segments that do opposite things.
With text
ButtonGroupText adds an inline label segment, like a prefix or unit, that merges into the control.
Props
ButtonGroup and ButtonGroupText also accept every native div prop, and ButtonGroupSeparator forwards the ai2 Separator props including orientation.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction on ButtonGroup. Horizontal collapses left/right radii and borders; vertical collapses top/bottom. Emitted as data-orientation. |
asChild | boolean | false | On ButtonGroupText, render the child element instead of a div via radix Slot, so the label can be a real element you control. |
ai2 Button Group: a segmented control for React buttons and inputs
The ai2 Button Group is a shadcn-compatible layout component for React that fuses a row or column of controls into one segmented unit. It wraps your ai2 Buttons, and optionally inputs, selects and text, then collapses the inner radii and borders so they read as a single control with shared edges instead of separate buttons sitting next to each other.
It ships through the shadcn registry format, so one CLI command, an MCP agent, or a copy-paste puts the source 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 Button Group?
It is a three-part set: ButtonGroup is the wrapper, ButtonGroupText is an inline label segment, and ButtonGroupSeparator draws a divider between segments. The wrapper is a role="group" container that lays its children out with flexbox and rounds only the outer corners.
The single axis is orientation (horizontal or vertical). Horizontal removes the left radius and border from every child after the first so adjacent edges merge; vertical does the same on the top edge. Because the group styles its children through selectors rather than cloning them, you compose it from ordinary ai2 Buttons and form controls without special wrappers.
Why use it
- One control from many: The group collapses the inner radii and borders of its children, so a set of buttons reads as a single segmented control instead of a loose row.
- Horizontal or vertical: One orientation prop switches between a row and a column, merging the correct edges either way and emitting data-orientation for further styling.
- Composes with real controls: It styles its children through selectors, so you drop in ai2 Buttons, Inputs, Selects and ButtonGroupText directly, with no cloning or forwarded refs to manage.
- Text and separator parts: ButtonGroupText adds an inline label segment (like a prefix or unit) and ButtonGroupSeparator draws a token-colored divider between segments.
- Agent-readable metadata: The registry item names the parts and the orientation axis in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, the ai2 Button and Separator dependencies and the @ai2/tokens theme to your project.
- Two orientations: orientation="horizontal" merges left/right edges; orientation="vertical" merges top/bottom edges and stacks the children in a column.
- ButtonGroupText segment: A bordered, muted label cell for prefixes, units or read-only context, with an asChild prop to render it as your own element.
- ButtonGroupSeparator divider: Wraps the ai2 Separator to draw a divider between segments, defaulting to a vertical rule that stretches to the group height.
- Icon-family agnostic: The parts lay out correctly with both lucide svg icons and remixicon i elements, so the segmented control looks right whichever family your app uses.
- Data attributes for styling: The wrapper exposes data-slot="button-group" and data-orientation, and the parts carry their own data-slot values, so you can target them from CSS without forking.
Production tips
- Use it for related actions: A button group implies the buttons belong together, like a toolbar or a set of view toggles. Do not group unrelated actions just to save space.
- Keep child variants consistent: Give every button in the group the same variant and size so the merged edges line up. Mixing outline and solid buttons breaks the single-control look.
- Label icon-only buttons: Icon buttons inside a group still need an aria-label so screen readers can name each segment. The group itself is a role="group" container, not a label.
- Reach for ToggleGroup for selection: Button Group is a layout that fuses controls. If you need single or multiple selection with pressed state, use ToggleGroup, which manages the selected value for you.
- Add a separator to break segments: Drop a ButtonGroupSeparator between buttons that do opposite things, like Prev and Next, so the divide is visible even though the edges are merged.
Works with the rest of ai2
The button group is a layout for the rest of the registry. Fill it with ai2 Button segments, add an Input or Select for a combined field-and-action control, and draw dividers with ai2 Separator through ButtonGroupSeparator.
When you need pressed, single or multiple selection rather than plain actions, reach for Toggle Group instead, and attach a Tooltip to each icon-only segment for a hint. Everything shares one token source, so the combinations stay visually consistent in both modes.