"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> );}pnpm dlx shadcn@latest add @herocn/button-groupimport { ButtonGroup, ButtonGroupSeparator, ButtonGroupText,} from "@/components/ui/button-group"<ButtonGroup> <Button>Button 1</Button> <Button>Button 2</Button></ButtonGroup>Use the following composition to build a ButtonGroup:
ButtonGroup├── Button or Input├── ButtonGroupSeparator└── ButtonGroupTextButtonGroup component sets role="group" on the container.Tab key to move focus between controls inside the group.aria-label or aria-labelledby.<ButtonGroup aria-label="Button group"> <Button>Button 1</Button> <Button>Button 2</Button></ButtonGroup>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.
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> );}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> );}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> );}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> );}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> );}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> );}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> );}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> );}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> );}Container that groups related controls with shared border and radius treatment.
| Prop | Type | Default |
|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" |
Layout direction for direct children. | ||
<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>Visually divides adjacent controls. Orientation follows the separator: use vertical (default) between horizontally arranged buttons, or horizontal inside a vertical group.
| Prop | Type | Default |
|---|---|---|
| orientation | "horizontal" | "vertical" | "vertical" |
Separator axis; should be orthogonal to the group orientation. | ||
<ButtonGroup> <Button>Button 1</Button> <ButtonGroupSeparator /> <Button>Button 2</Button></ButtonGroup>Static text or label region inside the group. Uses Base UI’s render prop (not asChild) to compose with a custom element.
| Prop | Type | Default |
|---|---|---|
| render | React.ReactElement | — |
Optional element to render as the root (e.g. a `Label`) while merging styles and behavior. | ||
<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> );}