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 @herocn/button-group

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 className="w-full" 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="tertiary">Follow</Button>			<DropdownMenu>				<DropdownMenuTrigger					render={<Button variant="tertiary" 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="tertiary">				<BotIcon /> Copilot			</Button>			<Popover>				<PopoverTrigger					render={						<Button variant="tertiary" 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..."							variant="secondary"							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>	);}