Shadcn Button Group for React and Tailwind
Visually connected buttons and related controls.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/button-group.jsonpnpm dlx shadcn@latest add https://kit.dev/r/button-group.jsonnpx shadcn@latest add https://kit.dev/r/button-group.jsonyarn shadcn@latest add https://kit.dev/r/button-group.json<Step>This component depends on Separator. Install it first if you haven't already. Pair it with Button.</Step>
Install the following dependencies:
bun add @ark-ui/react tailwind-variantspnpm add @ark-ui/react tailwind-variantsnpm install @ark-ui/react tailwind-variantsyarn add @ark-ui/react tailwind-variantsCopy and paste the following code into your project.
"use client";
import { ark } from "@ark-ui/react/factory";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
export const buttonGroupVariants = tv({
base: [
"m-0 min-w-0 border-0 p-0",
"flex w-fit items-stretch",
"*:not([class*='w-']):w-fit",
"*:not([class*='flex-']):flex-1",
"*:focus-visible:relative *:focus-visible:z-10",
"has-[>[data-slot=button-group]]:gap-2",
"has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md",
],
variants: {
orientation: {
horizontal: [
"[&>*:not(:first-child)]:rounded-s-none",
"[&>*:not(:first-child)]:border-s-0",
"[&>*:not(:last-child)]:rounded-e-none",
],
vertical: [
"flex-col",
"[&>*:not(:first-child)]:rounded-t-none",
"[&>*:not(:first-child)]:border-t-0",
"[&>*:not(:last-child)]:rounded-b-none [&>*:not(:last-child)]:shadow-none",
],
},
},
defaultVariants: {
orientation: "horizontal",
},
});
export interface ButtonGroupProps
extends React.ComponentProps<typeof ark.fieldset>,
VariantProps<typeof buttonGroupVariants> {}
export const ButtonGroup = (props: ButtonGroupProps) => {
const { className, orientation = "horizontal", ...rest } = props;
return (
<ark.fieldset
className={cn(buttonGroupVariants({ orientation }), className)}
data-orientation={orientation}
data-slot="button-group"
{...rest}
/>
);
};
export const ButtonGroupText = (
props: React.ComponentProps<typeof ark.div>
) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn(
"flex items-center gap-2 px-4",
"font-medium text-sm",
"rounded-md border bg-muted shadow-xs",
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className
)}
data-slot="button-group-text"
{...rest}
/>
);
};
export const ButtonGroupSeparator = (
props: React.ComponentProps<typeof Separator>
) => {
const { orientation = "vertical", className, ...rest } = props;
return (
<Separator
className={cn(
"relative",
"self-stretch",
"bg-input",
"data-[orientation=vertical]:h-auto",
"m-0!",
className
)}
data-slot="button-group-separator"
orientation={orientation}
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
ButtonGroup
├── Button | Input | nested ButtonGroup
├── ButtonGroupText
└── ButtonGroupSeparatorChildren share edges: first and last keep the outer radius, inner corners are squared, and adjoining start borders are removed.
Usage
import { Button } from "@/components/ui/button";
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@/components/ui/button-group";<ButtonGroup aria-label="Message actions">
<Button variant="outline">Archive</Button>
<Button variant="outline">Report</Button>
</ButtonGroup>Label the group with aria-label or aria-labelledby. The root is a fieldset.
Orientation
orientation is horizontal by default. Vertical stacks children and joins top/bottom edges.
Horizontal
Vertical
Examples
Nested
Nest groups to cluster related actions. Nested groups get a gap-2 between them.
Separator
Use ButtonGroupSeparator when buttons have no adjoining border (for example variant="secondary"). Outline buttons already share an edge, so they usually do not need a separator.
With text
ButtonGroupText is a non-interactive label inside the cluster (prefixes, units, captions).
With input
With menu
With popover
Guides
Grouping semantics
ButtonGroup is a fieldset. Name it (aria-label or aria-labelledby). Icon-only children still need aria-label on the Button.
Nested groups
An outer group that contains other ButtonGroups adds gap-2 and does not collapse radii across the inner clusters. Put aria-label on the outer group and on each inner cluster when the actions differ.
Separators
ButtonGroupSeparator defaults to orientation="vertical" so it splits a horizontal row. In a vertical group, pass orientation="horizontal".
Button Group vs Toggle Group vs Button
| Button Group | Toggle Group | Button | |
|---|---|---|---|
| Role | Visual cluster of independent actions | One or more pressed states | A single action |
| State | None of its own | value / onValueChange | disabled / isLoading |
RTL
Horizontal joining uses logical radii (rounded-s-none, rounded-e-none) and border-s-0, so the cluster follows text direction. Vertical joining uses top/bottom edges.
API Reference
shadcn.io Button Group is a styled fieldset (Ark factory), not a Zag machine. There is no context hook or root provider.
asChild merges props onto a single child element.
ButtonGroup
Root. Renders a fieldset.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Join along the inline axis or the block axis. |
asChild | boolean | false | Render the child element instead of a fieldset. |
className | string | - | Class names on the root. |
aria-label | string | - | Accessible name of the group. Prefer this or aria-labelledby. |
Native fieldset attributes (disabled, form, name, …) pass through. disabled on a fieldset disables descendant controls.
| Attribute | Description |
|---|---|
data-slot | button-group |
data-orientation | "horizontal" or "vertical" |
ButtonGroupText
Non-interactive label inside the cluster. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the text. |
| Attribute | Description |
|---|---|
data-slot | button-group-text |
ButtonGroupSeparator
Visual divider. Renders Separator (role="separator").
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "vertical" | vertical is a thin inline divider for a horizontal group. |
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the separator. |
| Attribute | Description |
|---|---|
data-slot | button-group-separator |
data-orientation | "horizontal" or "vertical" |
role | separator |
buttonGroupVariants
Exported tv() recipe. Same orientation key as ButtonGroup.
buttonGroupVariants({ orientation: "vertical" });ButtonGroupProps
Props of ButtonGroup: fieldset attributes plus orientation.
Accessibility
The root is a grouping fieldset. Give it an accessible name. It is not a composite widget: each button, input, or menu trigger stays in the normal tab order.
- Icon-only buttons:
aria-labelon the button. Decorative icons:aria-hidden="true". - Nested groups: name the outer group and each inner cluster when they represent different tasks.
ButtonGroupTextis not a substitute for a field label; pair inputs witharia-labelor Field.
Keyboard support
| Key | Description |
|---|---|
Tab | Move to the next control in the group. |
Shift + Tab | Move to the previous control. |
Enter / Space | Activate the focused button (or the child’s own keys, such as a menu trigger). |