Github

Button Group

A container that groups related buttons together with consistent styling.

"use client";

import {
	ArchiveIcon,
	ArrowLeftIcon,
	CalendarPlusIcon,
	ClockIcon,
	ListFilterIcon,
	MailCheckIcon,
	MoreHorizontalIcon,
	TagIcon,
	Trash2Icon,
} from "lucide-react";
import * as React from "react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuRadioGroup,
	DropdownMenuRadioItem,
	DropdownMenuSeparator,
	DropdownMenuSub,
	DropdownMenuSubContent,
	DropdownMenuSubTrigger,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export function ButtonGroupDemo() {
	const [label, setLabel] = React.useState("personal");

	return (
		<ButtonGroup>
			<ButtonGroup className="hidden sm:flex">
				<Button variant="tertiary" size="icon" aria-label="Go Back">
					<ArrowLeftIcon />
				</Button>
			</ButtonGroup>
			<ButtonGroup>
				<Button variant="tertiary">Archive</Button>
				<Button variant="tertiary">Report</Button>
			</ButtonGroup>
			<ButtonGroup>
				<Button variant="tertiary">Snooze</Button>
				<DropdownMenu>
					<DropdownMenuTrigger
						render={
							<Button
								variant="tertiary"
								size="icon"
								aria-label="More Options"
							/>
						}
					>
						<MoreHorizontalIcon />
					</DropdownMenuTrigger>
					<DropdownMenuContent align="end" className="w-40">
						<DropdownMenuGroup>
							<DropdownMenuItem>
								<MailCheckIcon />
								Mark as Read
							</DropdownMenuItem>
							<DropdownMenuItem>
								<ArchiveIcon />
								Archive
							</DropdownMenuItem>
						</DropdownMenuGroup>
						<DropdownMenuSeparator />
						<DropdownMenuGroup>
							<DropdownMenuItem>
								<ClockIcon />
								Snooze
							</DropdownMenuItem>
							<DropdownMenuItem>
								<CalendarPlusIcon />
								Add to Calendar
							</DropdownMenuItem>
							<DropdownMenuItem>
								<ListFilterIcon />
								Add to List
							</DropdownMenuItem>
							<DropdownMenuSub>
								<DropdownMenuSubTrigger>
									<TagIcon />
									Label As...
								</DropdownMenuSubTrigger>
								<DropdownMenuSubContent>
									<DropdownMenuRadioGroup
										value={label}
										onValueChange={setLabel}
									>
										<DropdownMenuRadioItem value="personal">
											Personal
										</DropdownMenuRadioItem>
										<DropdownMenuRadioItem value="work">
											Work
										</DropdownMenuRadioItem>
										<DropdownMenuRadioItem value="other">
											Other
										</DropdownMenuRadioItem>
									</DropdownMenuRadioGroup>
								</DropdownMenuSubContent>
							</DropdownMenuSub>
						</DropdownMenuGroup>
						<DropdownMenuSeparator />
						<DropdownMenuGroup>
							<DropdownMenuItem variant="destructive">
								<Trash2Icon />
								Trash
							</DropdownMenuItem>
						</DropdownMenuGroup>
					</DropdownMenuContent>
				</DropdownMenu>
			</ButtonGroup>
		</ButtonGroup>
	);
}

Installation

pnpm dlx shadcn@latest add https://herocn.dev/r/button-group.json

Usage

import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from "@/components/ui/button-group"
<ButtonGroup>
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

Composition

Use the following composition to build a ButtonGroup:

ButtonGroup
├── Button or Input
├── ButtonGroupSeparator
└── ButtonGroupText

Accessibility

  • The ButtonGroup component sets role="group" on the container.
  • Use the Tab key to move focus between controls inside the group.
  • Provide an accessible name with aria-label or aria-labelledby.
<ButtonGroup aria-label="Button group">
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

ButtonGroup vs selection controls

Use ButtonGroup when each control performs a one-shot action (navigate, submit, open a menu). When you need a single selected option among several (pressed / active state), use Tabs, a radio group, or another selection pattern instead.

Examples

Orientation

Set the orientation prop to change the layout.

import { MinusIcon, PlusIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";

export function ButtonGroupOrientation() {
	return (
		<ButtonGroup
			orientation="vertical"
			aria-label="Media controls"
			className="h-fit"
		>
			<Button variant="tertiary" size="icon">
				<PlusIcon />
			</Button>
			<Button variant="tertiary" size="icon">
				<MinusIcon />
			</Button>
		</ButtonGroup>
	);
}

Size

Control sizing with the size prop on each Button.

import { PlusIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";

export function ButtonGroupSize() {
	return (
		<div className="flex flex-col items-start gap-8">
			<ButtonGroup>
				<Button variant="tertiary" size="sm">
					Small
				</Button>
				<Button variant="tertiary" size="sm">
					Button
				</Button>
				<Button variant="tertiary" size="sm">
					Group
				</Button>
				<Button variant="tertiary" size="icon-sm">
					<PlusIcon />
				</Button>
			</ButtonGroup>
			<ButtonGroup>
				<Button variant="tertiary">Default</Button>
				<Button variant="tertiary">Button</Button>
				<Button variant="tertiary">Group</Button>
				<Button variant="tertiary" size="icon">
					<PlusIcon />
				</Button>
			</ButtonGroup>
			<ButtonGroup>
				<Button variant="tertiary" size="lg">
					Large
				</Button>
				<Button variant="tertiary" size="lg">
					Button
				</Button>
				<Button variant="tertiary" size="lg">
					Group
				</Button>
				<Button variant="tertiary" size="icon-lg">
					<PlusIcon />
				</Button>
			</ButtonGroup>
		</div>
	);
}

Separator

ButtonGroupSeparator visually divides items. Outline buttons often rely on shared borders; for other variants, a separator can clarify hierarchy.

import { Button } from "@/components/ui/button";
import {
	ButtonGroup,
	ButtonGroupSeparator,
} from "@/components/ui/button-group";

export function ButtonGroupSeparatorDemo() {
	return (
		<div className="flex flex-col items-center justify-center gap-4">
			<ButtonGroup orientation="horizontal">
				<Button variant="secondary" size="sm">
					Copy
				</Button>
				<ButtonGroupSeparator />
				<Button variant="secondary" size="sm">
					Paste
				</Button>
			</ButtonGroup>
			<ButtonGroup orientation="vertical" className="w-fit">
				<Button variant="secondary" size="sm">
					Copy
				</Button>
				<ButtonGroupSeparator orientation="horizontal" />
				<Button variant="secondary" size="sm">
					Paste
				</Button>
			</ButtonGroup>
		</div>
	);
}

Split

Pair a primary action with an icon button, separated by ButtonGroupSeparator.

import { PlusIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
	ButtonGroup,
	ButtonGroupSeparator,
} from "@/components/ui/button-group";

export function ButtonGroupSplit() {
	return (
		<div className="flex flex-col items-center justify-center gap-4">
			<ButtonGroup orientation="horizontal">
				<Button variant="secondary">Button</Button>
				<ButtonGroupSeparator />
				<Button size="icon" variant="secondary">
					<PlusIcon />
				</Button>
			</ButtonGroup>
			<ButtonGroup orientation="vertical" className="h-fit">
				<Button variant="secondary">Button</Button>
				<ButtonGroupSeparator orientation="horizontal" />
				<Button size="icon" variant="secondary">
					<PlusIcon />
				</Button>
			</ButtonGroup>
		</div>
	);
}

Input

Place an Input beside buttons in the same group.

import { SearchIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { Input } from "@/components/ui/input";

export function ButtonGroupInput() {
	return (
		<ButtonGroup className="max-w-sm">
			<Input variant="secondary" placeholder="Search..." />
			<Button variant="tertiary" aria-label="Search">
				<SearchIcon />
			</Button>
		</ButtonGroup>
	);
}

Input group

Combine with InputGroup for richer controls (addons, inline buttons).

"use client";

import { AudioLinesIcon, PlusIcon } from "lucide-react";
import * as React from "react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupButton,
	InputGroupInput,
} from "@/components/ui/input-group";
import {
	Tooltip,
	TooltipContent,
	TooltipTrigger,
} from "@/components/ui/tooltip";

export function ButtonGroupInputGroup() {
	const [voiceEnabled, setVoiceEnabled] = React.useState(false);

	return (
		<ButtonGroup>
			<ButtonGroup>
				<Button variant="outline" size="icon">
					<PlusIcon />
				</Button>
			</ButtonGroup>
			<ButtonGroup>
				<InputGroup>
					<InputGroupInput
						placeholder={
							voiceEnabled ? "Record and send audio..." : "Send a message..."
						}
						disabled={voiceEnabled}
					/>
					<InputGroupAddon align="inline-end">
						<Tooltip>
							<TooltipTrigger
								render={
									<InputGroupButton
										onClick={() => setVoiceEnabled(!voiceEnabled)}
										size="icon-sm"
										data-active={voiceEnabled}
										className="data-[active=true]:bg-orange-100 data-[active=true]:text-orange-700 dark:data-[active=true]:bg-orange-800 dark:data-[active=true]:text-orange-100"
										aria-pressed={voiceEnabled}
									/>
								}
							>
								<AudioLinesIcon />
							</TooltipTrigger>
							<TooltipContent>Voice Mode</TooltipContent>
						</Tooltip>
					</InputGroupAddon>
				</InputGroup>
			</ButtonGroup>
		</ButtonGroup>
	);
}

Attach a DropdownMenu for a split button or overflow actions.

import {
	AlertTriangleIcon,
	CheckIcon,
	ChevronDownIcon,
	CopyIcon,
	ShareIcon,
	TrashIcon,
	UserRoundXIcon,
	VolumeOffIcon,
} from "lucide-react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuSeparator,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export function ButtonGroupDropdown() {
	return (
		<ButtonGroup>
			<Button variant="outline">Follow</Button>
			<DropdownMenu>
				<DropdownMenuTrigger
					render={<Button variant="outline" className="ps-2!" />}
				>
					<ChevronDownIcon />
				</DropdownMenuTrigger>
				<DropdownMenuContent align="end" className="w-44">
					<DropdownMenuGroup>
						<DropdownMenuItem>
							<VolumeOffIcon />
							Mute Conversation
						</DropdownMenuItem>
						<DropdownMenuItem>
							<CheckIcon />
							Mark as Read
						</DropdownMenuItem>
						<DropdownMenuItem>
							<AlertTriangleIcon />
							Report Conversation
						</DropdownMenuItem>
						<DropdownMenuItem>
							<UserRoundXIcon />
							Block User
						</DropdownMenuItem>
						<DropdownMenuItem>
							<ShareIcon />
							Share Conversation
						</DropdownMenuItem>
						<DropdownMenuItem>
							<CopyIcon />
							Copy Conversation
						</DropdownMenuItem>
					</DropdownMenuGroup>
					<DropdownMenuSeparator />
					<DropdownMenuGroup>
						<DropdownMenuItem variant="destructive">
							<TrashIcon />
							Delete Conversation
						</DropdownMenuItem>
					</DropdownMenuGroup>
				</DropdownMenuContent>
			</DropdownMenu>
		</ButtonGroup>
	);
}

Select

Use alongside Select for compound fields such as currency plus amount.

"use client";

import { ArrowRightIcon } from "lucide-react";
import * as React from "react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { Input } from "@/components/ui/input";
import {
	Select,
	SelectContent,
	SelectGroup,
	SelectItem,
	SelectTrigger,
} from "@/components/ui/select";

const CURRENCIES = [
	{ label: "US Dollar", value: "$" },
	{ label: "Euro", value: "€" },
	{ label: "British Pound", value: "£" },
];

export function ButtonGroupSelect() {
	const [currency, setCurrency] = React.useState("$");

	return (
		<ButtonGroup>
			<ButtonGroup>
				<Select
					items={CURRENCIES}
					value={currency}
					variant="secondary"
					onValueChange={(value) => setCurrency(value as string)}
				>
					<SelectTrigger>{currency}</SelectTrigger>
					<SelectContent align="start">
						<SelectGroup>
							{CURRENCIES.map((item) => (
								<SelectItem key={item.value} value={item.value}>
									{item.value}{" "}
									<span className="text-muted-foreground">{item.label}</span>
								</SelectItem>
							))}
						</SelectGroup>
					</SelectContent>
				</Select>
				<Input variant="secondary" placeholder="10.00" pattern="[0-9]*" />
			</ButtonGroup>
			<ButtonGroup>
				<Button aria-label="Send" size="icon" variant="tertiary">
					<ArrowRightIcon />
				</Button>
			</ButtonGroup>
		</ButtonGroup>
	);
}

Popover

Pair with Popover for secondary panels tied to the group.

import { BotIcon, ChevronDownIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
	Field,
	FieldDescription,
	FieldLabel,
} from "@/components/ui/field";
import {
	Popover,
	PopoverContent,
	PopoverDescription,
	PopoverHeader,
	PopoverTitle,
	PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";

export function ButtonGroupPopover() {
	return (
		<ButtonGroup>
			<Button variant="outline">
				<BotIcon /> Copilot
			</Button>
			<Popover>
				<PopoverTrigger
					render={
						<Button variant="outline" size="icon" aria-label="Open Popover" />
					}
				>
					<ChevronDownIcon />
				</PopoverTrigger>
				<PopoverContent align="end" className="rounded-xl text-sm">
					<PopoverHeader>
						<PopoverTitle>Start a new task with Copilot</PopoverTitle>
						<PopoverDescription>
							Describe your task in natural language.
						</PopoverDescription>
					</PopoverHeader>
					<Field>
						<FieldLabel htmlFor="button-group-task" className="sr-only">
							Task Description
						</FieldLabel>
						<Textarea
							id="button-group-task"
							placeholder="I need to..."
							className="resize-none"
						/>
						<FieldDescription>
							Copilot will open a pull request for review.
						</FieldDescription>
					</Field>
				</PopoverContent>
			</Popover>
		</ButtonGroup>
	);
}

RTL

To enable RTL support in shadcn/ui, see the RTL configuration guide.

"use client";

import {
	ArchiveIcon,
	ArrowLeftIcon,
	CalendarPlusIcon,
	ClockIcon,
	ListFilterIcon,
	MailCheckIcon,
	MoreHorizontalIcon,
	TagIcon,
	Trash2Icon,
} from "lucide-react";
import * as React from "react";

import {
	type Translations,
	useTranslation,
} from "@/components/language-selector";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuRadioGroup,
	DropdownMenuRadioItem,
	DropdownMenuSeparator,
	DropdownMenuSub,
	DropdownMenuSubContent,
	DropdownMenuSubTrigger,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

const translations: Translations = {
	en: {
		dir: "ltr",
		values: {
			archive: "Archive",
			report: "Report",
			snooze: "Snooze",
			markAsRead: "Mark as Read",
			addToCalendar: "Add to Calendar",
			addToList: "Add to List",
			labelAs: "Label As...",
			personal: "Personal",
			work: "Work",
			other: "Other",
			trash: "Trash",
		},
	},
	ar: {
		dir: "rtl",
		values: {
			archive: "أرشفة",
			report: "تقرير",
			snooze: "تأجيل",
			markAsRead: "وضع علامة كمقروء",
			addToCalendar: "إضافة إلى التقويم",
			addToList: "إضافة إلى القائمة",
			labelAs: "تصنيف كـ...",
			personal: "شخصي",
			work: "عمل",
			other: "آخر",
			trash: "سلة المهملات",
		},
	},
	he: {
		dir: "rtl",
		values: {
			archive: "ארכיון",
			report: "דוח",
			snooze: "דחה",
			markAsRead: "סמן כנקרא",
			addToCalendar: "הוסף ליומן",
			addToList: "הוסף לרשימה",
			labelAs: "תייג כ...",
			personal: "אישי",
			work: "עבודה",
			other: "אחר",
			trash: "פח",
		},
	},
};

export function ButtonGroupRtl() {
	const { dir, t, language } = useTranslation(translations, "ar");
	const [label, setLabel] = React.useState("personal");

	return (
		<div dir={dir}>
			<ButtonGroup>
				<ButtonGroup className="hidden sm:flex">
					<Button variant="tertiary" size="icon" aria-label="Go Back">
						<ArrowLeftIcon className="rtl:rotate-180" />
					</Button>
				</ButtonGroup>
				<ButtonGroup>
					<Button variant="tertiary">{t.archive}</Button>
					<Button variant="tertiary">{t.report}</Button>
				</ButtonGroup>
				<ButtonGroup>
					<Button variant="tertiary">{t.snooze}</Button>
					<DropdownMenu>
						<DropdownMenuTrigger
							render={
								<Button
									variant="tertiary"
									size="icon"
									aria-label="More Options"
								/>
							}
						>
							<MoreHorizontalIcon />
						</DropdownMenuTrigger>
						<DropdownMenuContent
							align={dir === "rtl" ? "start" : "end"}
							data-lang={dir === "rtl" ? language : undefined}
							dir={dir}
							className="w-40"
						>
							<DropdownMenuGroup>
								<DropdownMenuItem>
									<MailCheckIcon />
									{t.markAsRead}
								</DropdownMenuItem>
								<DropdownMenuItem>
									<ArchiveIcon />
									{t.archive}
								</DropdownMenuItem>
							</DropdownMenuGroup>
							<DropdownMenuSeparator />
							<DropdownMenuGroup>
								<DropdownMenuItem>
									<ClockIcon />
									{t.snooze}
								</DropdownMenuItem>
								<DropdownMenuItem>
									<CalendarPlusIcon />
									{t.addToCalendar}
								</DropdownMenuItem>
								<DropdownMenuItem>
									<ListFilterIcon />
									{t.addToList}
								</DropdownMenuItem>
								<DropdownMenuSub>
									<DropdownMenuSubTrigger>
										<TagIcon />
										{t.labelAs}
									</DropdownMenuSubTrigger>
									<DropdownMenuSubContent
										dir={dir}
										data-lang={dir === "rtl" ? language : undefined}
									>
										<DropdownMenuRadioGroup
											value={label}
											onValueChange={setLabel}
										>
											<DropdownMenuRadioItem value="personal">
												{t.personal}
											</DropdownMenuRadioItem>
											<DropdownMenuRadioItem value="work">
												{t.work}
											</DropdownMenuRadioItem>
											<DropdownMenuRadioItem value="other">
												{t.other}
											</DropdownMenuRadioItem>
										</DropdownMenuRadioGroup>
									</DropdownMenuSubContent>
								</DropdownMenuSub>
							</DropdownMenuGroup>
							<DropdownMenuSeparator />
							<DropdownMenuGroup>
								<DropdownMenuItem variant="destructive">
									<Trash2Icon />
									{t.trash}
								</DropdownMenuItem>
							</DropdownMenuGroup>
						</DropdownMenuContent>
					</DropdownMenu>
				</ButtonGroup>
			</ButtonGroup>
		</div>
	);
}

API Reference

ButtonGroup

Container that groups related controls with shared border and radius treatment.

PropTypeDefault
<ButtonGroup>
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

Nest multiple ButtonGroup components to create spaced clusters. See the nested example.

<ButtonGroup>
  <ButtonGroup>{/* … */}</ButtonGroup>
  <ButtonGroup>{/* … */}</ButtonGroup>
</ButtonGroup>

ButtonGroupSeparator

Visually divides adjacent controls. Orientation follows the separator: use vertical (default) between horizontally arranged buttons, or horizontal inside a vertical group.

PropTypeDefault
<ButtonGroup>
  <Button>Button 1</Button>
  <ButtonGroupSeparator />
  <Button>Button 2</Button>
</ButtonGroup>

ButtonGroupText

Static text or label region inside the group. Uses Base UI’s render prop (not asChild) to compose with a custom element.

PropTypeDefault
<ButtonGroup>
  <ButtonGroupText>Text</ButtonGroupText>
  <Button>Button</Button>
</ButtonGroup>

Use render to associate text with a form control:

import {
	ButtonGroup,
	ButtonGroupText,
} from "@/components/ui/button-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

export function ButtonGroupTextDemo() {
	return (
		<ButtonGroup className="max-w-sm">
			<ButtonGroupText
				render={<Label htmlFor="button-group-text-name">Text</Label>}
			/>
			<Input
				id="button-group-text-name"
				variant="secondary"
				placeholder="Type something here..."
			/>
		</ButtonGroup>
	);
}
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"
import { Label } from "@/components/ui/label"

export function ButtonGroupTextExample() {
  return (
    <ButtonGroup>
      <ButtonGroupText render={<Label htmlFor="name">Text</Label>} />
      <Input placeholder="Type something here..." id="name" />
    </ButtonGroup>
  )
}