Bring related actions together in one compact control. Outer corners stay rounded while shared edges remain clean and square.
import { Button, ButtonGroup, buttonVariants,} from "@/components/ui/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"export function Example() { return ( <div className="flex flex-wrap items-center gap-2"> <Button variant="outline" size="icon" aria-label="Back"> <ArrowLeftIcon /> </Button> <ButtonGroup> <Button variant="outline">Archive</Button> <Button variant="outline">Report</Button> </ButtonGroup> <ButtonGroup> <Button variant="outline">Snooze</Button> <DropdownMenu> <DropdownMenuTrigger aria-label="More actions" className={buttonVariants({ variant: "outline", size: "icon" })} > <MoreHorizontalIcon /> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem> <CheckIcon /> Mark as done </DropdownMenuItem> <DropdownMenuItem> <ArchiveIcon /> Move to archive </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem> <ClockIcon /> Remind me later </DropdownMenuItem> <DropdownMenuItem> <PinIcon /> Pin this scene </DropdownMenuItem> <DropdownMenuItem> <ShareIcon /> Copy share link </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </ButtonGroup> </div> )}Installation
npx @arctis-sh/@arctis-sh/ui@latest add button-groupButtonGroup ships with Button and is also exported from @arctis-sh/ui/button.
Usage
import { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText,} from "@/components/ui/button"<ButtonGroup> <Button variant="outline">Archive</Button> <Button variant="outline">Share</Button></ButtonGroup>Composition
ButtonGroup ├── Button or Input ├── ButtonGroupSeparator └── ButtonGroupText
Accessibility
ButtonGroup sets role="group". Tab moves between its buttons. Give the group a label with aria-label or aria-labelledby.
<ButtonGroup aria-label="Document actions"> <Button variant="outline">Archive</Button> <Button variant="outline">Share</Button></ButtonGroup>Orientation
Set orientation to stack the segments instead of placing them side by side.
import { Button, ButtonGroup } from "@/components/ui/button"export function Example() { return ( <ButtonGroup orientation="vertical" aria-label="Volume"> <Button variant="outline" size="icon" aria-label="Increase"> <PlusIcon /> </Button> <Button variant="outline" size="icon" aria-label="Decrease"> <MinusIcon /> </Button> </ButtonGroup> )}Size
Set size on each button to control the group's density.
import { Button, ButtonGroup } from "@/components/ui/button"export function Example() { return ( <div className="flex flex-col items-start gap-4"> <ButtonGroup> <Button variant="outline" size="sm"> Small </Button> <Button variant="outline" size="sm"> Button </Button> <Button variant="outline" size="sm"> Group </Button> <Button variant="outline" size="icon-sm" aria-label="Add"> <PlusIcon /> </Button> </ButtonGroup> <ButtonGroup> <Button variant="outline">Default</Button> <Button variant="outline">Button</Button> <Button variant="outline">Group</Button> <Button variant="outline" size="icon" aria-label="Add"> <PlusIcon /> </Button> </ButtonGroup> <ButtonGroup> <Button variant="outline" size="lg"> Large </Button> <Button variant="outline" size="lg"> Button </Button> <Button variant="outline" size="lg"> Group </Button> <Button variant="outline" size="icon-lg" aria-label="Add"> <PlusIcon /> </Button> </ButtonGroup> </div> )}Nested
Nest ButtonGroup components to arrange related clusters with space between them.
import { Button, ButtonGroup } from "@/components/ui/button"export function Example() { return ( <ButtonGroup> <ButtonGroup> <Button variant="outline">1</Button> <Button variant="outline">2</Button> <Button variant="outline">3</Button> </ButtonGroup> <ButtonGroup> <Button variant="outline" size="icon" aria-label="Add"> <PlusIcon /> </Button> <Button variant="outline" size="icon" aria-label="Voice"> <AudioLinesIcon /> </Button> </ButtonGroup> </ButtonGroup> )}Separator
ButtonGroupSeparator adds a stronger division between children. Outline groups usually do not need one.
import { Button, ButtonGroup, ButtonGroupSeparator,} from "@/components/ui/button"export function Example() { return ( <ButtonGroup> <Button variant="secondary">Copy</Button> <ButtonGroupSeparator /> <Button variant="secondary">Paste</Button> </ButtonGroup> )}Split
Pair a primary action with a separate menu trigger.
import { Button, ButtonGroup, ButtonGroupSeparator, buttonVariants,} from "@/components/ui/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"export function Example() { return ( <ButtonGroup> <Button variant="secondary"> <PlusIcon data-icon="inline-start" /> Button </Button> <ButtonGroupSeparator /> <DropdownMenu> <DropdownMenuTrigger aria-label="More options" className={buttonVariants({ variant: "secondary", size: "icon" })} > <ChevronDownIcon /> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem>Save</DropdownMenuItem> <DropdownMenuItem>Save as draft</DropdownMenuItem> <DropdownMenuItem>Export</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </ButtonGroup> )}Input
Place an input between supporting buttons.
import { Button, ButtonGroup } from "@/components/ui/button"export function Example() { return ( <ButtonGroup className="w-full max-w-sm"> <input placeholder="Search…" className="h-9 min-w-0 flex-1 border border-border bg-transparent px-3 text-sm outline-none" /> <Button variant="outline" size="icon" aria-label="Search"> <SearchIcon /> </Button> </ButtonGroup> )}Input group
Keep the add control separate while joining the message field to its voice action.
import { Button, ButtonGroup } from "@/components/ui/button"export function Example() { return ( <div className="flex w-full max-w-sm items-center gap-2"> <Button variant="outline" size="icon" aria-label="Add"> <PlusIcon /> </Button> <ButtonGroup className="min-w-0 flex-1"> <input placeholder="Send a message…" className="h-9 min-w-0 flex-1 border border-border bg-transparent px-3 text-sm outline-none" /> <Button variant="outline" size="icon" aria-label="Voice message"> <AudioLinesIcon /> </Button> </ButtonGroup> </div> )}Dropdown menu
Use DropdownMenu to create a split action.
import { Button, ButtonGroup, buttonVariants,} from "@/components/ui/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"export function Example() { return ( <ButtonGroup> <Button variant="outline">Follow</Button> <DropdownMenu> <DropdownMenuTrigger aria-label="Follow options" className={buttonVariants({ variant: "outline", size: "icon" })} > <ChevronDownIcon /> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem>Mute notifications</DropdownMenuItem> <DropdownMenuItem>Unfollow</DropdownMenuItem> <DropdownMenuItem>Block</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </ButtonGroup> )}Select
Pair a menu trigger with an input for a compact compound field.
import { useState } from "react"import { Button, ButtonGroup, buttonVariants,} from "@/components/ui/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"export function Example() { const [currency, setCurrency] = useState("$") return ( <ButtonGroup> <DropdownMenu> <DropdownMenuTrigger className={buttonVariants({ variant: "outline" })} > {currency} <ChevronDownIcon data-icon="inline-end" /> </DropdownMenuTrigger> <DropdownMenuContent align="start"> <DropdownMenuItem onClick={() => setCurrency("$")}>$</DropdownMenuItem> <DropdownMenuItem onClick={() => setCurrency("€")}>€</DropdownMenuItem> <DropdownMenuItem onClick={() => setCurrency("£")}>£</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> <input placeholder="0.00" className="h-9 w-24 border border-border bg-transparent px-3 text-sm outline-none" /> <Button variant="outline">Pay</Button> </ButtonGroup> )}Popover
Open richer content from a dedicated chevron segment.
import { Button, ButtonGroup, buttonVariants,} from "@/components/ui/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"export function Example() { return ( <ButtonGroup> <Button variant="outline"> <BotIcon data-icon="inline-start" /> Copilot </Button> <DropdownMenu> <DropdownMenuTrigger aria-label="Open Copilot" className={buttonVariants({ variant: "outline", size: "icon" })} > <ChevronDownIcon /> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-64 p-3"> <p className="m-0 mb-2 text-[13px] text-muted-foreground"> Ask Copilot to draft, explain, or refactor. </p> <textarea rows={3} placeholder="Ask anything…" className="m-0 block w-full resize-none rounded-md border border-border bg-transparent px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground" /> </DropdownMenuContent> </DropdownMenu> </ButtonGroup> )}API Reference
ButtonGroup
| Prop | Type | Default |
|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" |
| className | string | — |
Nest multiple groups when related clusters need a little separation. See Nested above.
<ButtonGroup> <Button variant="outline">Left</Button> <Button variant="outline">Right</Button></ButtonGroup>ButtonGroupSeparator
| Prop | Type | Default |
|---|---|---|
| orientation | "horizontal" | "vertical" | follows group |
| className | string | — |
<ButtonGroup> <Button variant="outline">Save</Button> <ButtonGroupSeparator /> <Button variant="outline">Cancel</Button></ButtonGroup>ButtonGroupText
| Prop | Type | Default |
|---|---|---|
| asChild | boolean | false |
| className | string | — |
<ButtonGroup> <ButtonGroupText>https://</ButtonGroupText> <Button variant="outline">Open</Button></ButtonGroup>