Button Group
Horizontal or vertical group that visually joins adjacent buttons into a single control.
Examples
#Dropdown Menu
#Create a split button group with a DropdownMenu component.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@codefast/ui/dropdown-menu";
import {
AlertTriangleIcon,
CheckIcon,
ChevronDownIcon,
CopyIcon,
ShareIcon,
TrashIcon,
UserRoundXIcon,
VolumeOffIcon,
} from "lucide-react";
export function ButtonGroupDropdown() {
return (
<ButtonGroup>
<Button variant="outline">Follow</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ps-2!">
<ChevronDownIcon />
</Button>
</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>
);
}
Input
#Wrap an Input component with buttons.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { Input } from "@codefast/ui/input";
import { SearchIcon } from "lucide-react";
export function ButtonGroupInput() {
return (
<ButtonGroup>
<Input placeholder="Search..." />
<Button variant="outline" aria-label="Search">
<SearchIcon />
</Button>
</ButtonGroup>
);
}
Input Group
#Wrap an InputGroup component to create complex input layouts.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@codefast/ui/input-group";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@codefast/ui/tooltip";
import { AudioLinesIcon, PlusIcon } from "lucide-react";
import * as React from "react";
export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false);
return (
<TooltipProvider>
<ButtonGroup className="[--radius:9999rem]">
<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 asChild>
<InputGroupButton
onClick={() => setVoiceEnabled(!voiceEnabled)}
size="icon-xs"
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 />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
</TooltipProvider>
);
}
Nested
#Nest <ButtonGroup> components to create button groups with spacing.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@codefast/ui/input-group";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@codefast/ui/tooltip";
import { AudioLinesIcon, PlusIcon } from "lucide-react";
export function ButtonGroupNested() {
return (
<TooltipProvider>
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<InputGroup>
<InputGroupInput placeholder="Send a message..." />
<Tooltip>
<TooltipTrigger asChild>
<InputGroupAddon align="inline-end">
<AudioLinesIcon />
</InputGroupAddon>
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
</TooltipProvider>
);
}
Orientation
#Set the orientation prop to change the button group layout.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { MinusIcon, PlusIcon } from "lucide-react";
export function ButtonGroupOrientation() {
return (
<ButtonGroup orientation="vertical" aria-label="Media controls" className="h-fit">
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
<Button variant="outline" size="icon">
<MinusIcon />
</Button>
</ButtonGroup>
);
}
Popover
#Use with a Popover component.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { Field, FieldDescription, FieldLabel } from "@codefast/ui/field";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@codefast/ui/popover";
import { Textarea } from "@codefast/ui/textarea";
import { BotIcon, ChevronDownIcon } from "lucide-react";
export function ButtonGroupPopover() {
return (
<ButtonGroup>
<Button variant="outline">
<BotIcon /> Copilot
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" aria-label="Open Popover">
<ChevronDownIcon />
</Button>
</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="task" className="sr-only">
Task Description
</FieldLabel>
<Textarea id="task" placeholder="I need to..." className="resize-none" />
<FieldDescription>Copilot will open a pull request for review.</FieldDescription>
</Field>
</PopoverContent>
</Popover>
</ButtonGroup>
);
}
RTL
#Right-to-left layout support for languages such as Arabic and Hebrew.
Translations are AI-generated for demonstration and may be imperfect.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@codefast/ui/dropdown-menu";
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react";
import * as React from "react";
import type { Translations } from "#/features/components-catalog/components/detail/language";
import { useTranslation } from "#/features/components-catalog/components/detail/language-context";
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="outline" size="icon" aria-label="Go Back">
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">{t.archive}</Button>
<Button variant="outline">{t.report}</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">{t.snooze}</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align={dir === "rtl" ? "end" : "end"}
data-lang={dir === "rtl" ? language : undefined}
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 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>
);
}
Select
#Pair with a Select component.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { Input } from "@codefast/ui/input";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger } from "@codefast/ui/select";
import { ArrowRightIcon } from "lucide-react";
import * as React from "react";
const CURRENCIES = [
{
value: "$",
label: "US Dollar",
},
{
value: "€",
label: "Euro",
},
{
value: "£",
label: "British Pound",
},
];
export function ButtonGroupSelect() {
const [currency, setCurrency] = React.useState("$");
return (
<ButtonGroup>
<ButtonGroup>
<Select value={currency} onValueChange={setCurrency}>
<SelectTrigger className="font-mono">{currency}</SelectTrigger>
<SelectContent className="min-w-24">
<SelectGroup>
{CURRENCIES.map((currency) => (
<SelectItem key={currency.value} value={currency.value}>
{currency.value} <span className="text-muted-foreground">{currency.label}</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Input placeholder="10.00" pattern="[0-9]*" />
</ButtonGroup>
<ButtonGroup>
<Button aria-label="Send" size="icon" variant="outline">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
);
}
Separator
#Buttons with variant outline do not need a separator since they have a border. For other variants, a separator is recommended to improve the visual hierarchy.
import { Button } from "@codefast/ui/button";
import { ButtonGroup, ButtonGroupSeparator } from "@codefast/ui/button-group";
export function ButtonGroupSeparatorDemo() {
return (
<ButtonGroup>
<Button variant="secondary" size="sm">
Copy
</Button>
<ButtonGroupSeparator />
<Button variant="secondary" size="sm">
Paste
</Button>
</ButtonGroup>
);
}
Size
#Control the size of buttons using the size prop on individual buttons.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
import { PlusIcon } from "lucide-react";
export function ButtonGroupSize() {
return (
<div className="flex flex-col items-start gap-8">
<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">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Default</Button>
<Button variant="outline">Button</Button>
<Button variant="outline">Group</Button>
<Button variant="outline" size="icon">
<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">
<PlusIcon />
</Button>
</ButtonGroup>
</div>
);
}
Split
#Create a split button group by adding two buttons separated by a ButtonGroupSeparator.
import { Button } from "@codefast/ui/button";
import { ButtonGroup, ButtonGroupSeparator } from "@codefast/ui/button-group";
import { PlusIcon } from "lucide-react";
export function ButtonGroupSplit() {
return (
<ButtonGroup>
<Button variant="secondary">Button</Button>
<ButtonGroupSeparator />
<Button size="icon" variant="secondary">
<PlusIcon />
</Button>
</ButtonGroup>
);
}
Usage
#The minimal import and composition — see Examples below for styled, real-world variants.
import { Button } from "@codefast/ui/button";
import { ButtonGroup } from "@codefast/ui/button-group";
export function ButtonGroupUsage() {
return (
<ButtonGroup>
<Button variant="outline">Copy</Button>
<Button variant="outline">Paste</Button>
<Button variant="outline">Cut</Button>
</ButtonGroup>
);
}
Anatomy
#How the parts nest — every slot the component exposes, in composition order.
Features
#- Plain role="group" wrapper — drop in ordinary Button (or Select/Input/InputGroup) children with no special prop needed on them.
- orientation="vertical" stacks the segment into a column instead of a row.
- Nest a ButtonGroup inside another to add a visual gap between two joined clusters.
API reference
#Props for each part of the component. All native element props are also forwarded.
ButtonGroup
Joins adjacent buttons into one visual control.
orientation"horizontal" | "vertical"Lay the buttons in a row or a column.
Default
"horizontal"
ButtonGroupText
A non-interactive, non-button label inside the group.
asChildbooleanMerge props onto the immediate child instead of rendering a div.
Default
falsechildrenReactNodeThe label content.
ButtonGroupSeparator
A visual divider between segments, for buttons that lack their own border.
orientation"horizontal" | "vertical"Match the separator's axis to the group's orientation.
Default
"vertical"
Accessibility
#Built to be keyboard-navigable and screen-reader friendly out of the box.
- Grouping is visual — each button keeps its own role and label.
- For one-of-many or multi-select toggles, use a Toggle Group instead.
- Keep related actions together; don’t mix unrelated buttons.
Guidelines
#Conventions that keep usage consistent across an app.
Do
- Group closely-related actions (copy/paste, prev/next).
- Add a ButtonGroupText label for split controls.
Don’t
- Don’t put toggle state here — use Toggle Group.
- Don’t cram unrelated actions into one group.
Related
#Related components
Explore further
Ready to integrate?
Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.