ButtonGroup
ButtonGroup joins related actions into one connected control, with a separator drawn between every pair of items.
Use it for action clusters that belong together, such as row actions, a toolbar of icon actions, or a split button. ButtonGroup holds no selection state; when one item has to stay selected, use SegmentedControl instead.
Quick Start
- Installation
npm install @adaptavant/eds-core- Import
import { ButtonGroup } from '@adaptavant/eds-core';
Basic Usage
Use Button and IconButton directly as children of a ButtonGroup, and name the group with aria-label. size and variant are set on the group and applied to every item, so there is no need to repeat them.
<ButtonGroup aria-label="Text formatting">
<Button>Cut</Button>
<Button>Copy</Button>
<Button>Paste</Button>
</ButtonGroup>
Setting size or variant on an individual item overrides the group for that item. A size override changes the item's height and breaks the joined shape, so reach for it only when that is what you want.
Icon Items
Use IconButton for icon-only actions. Each item needs its own aria-label.
<ButtonGroup aria-label="Row actions">
<IconButton aria-label="Edit" icon={EditIcon} />
<IconButton aria-label="Duplicate" icon={DuplicateIcon} />
<IconButton aria-label="Delete" icon={DeleteIcon} />
</ButtonGroup>
Split Button
Join a text item to an icon item to build a split button, where the primary action sits next to a trigger for related actions.
<ButtonGroup aria-label="Save options" variant="accentPrimary">
<Button>Save</Button>
<IconButton
aria-expanded={false}
aria-haspopup="menu"
aria-label="More save options"
icon={DropdownDownIcon}
/>
</ButtonGroup>
Wrap the icon item alone in a DropdownMenu and spread its triggerProps onto the IconButton. DropdownMenu renders no element of its own, so the group still counts it as one item and the trigger still picks up the group's size and variant. Do not use DropdownMenuTrigger here: it renders its own button with its own size and variant, which will not match the rest of the group.
<ButtonGroup aria-label="Save options" variant="accentPrimary">
<Button>Save</Button>
<DropdownMenu>
{({ isMenuOpen, triggerProps }) => (
<>
<IconButton
aria-label="More save options"
icon={isMenuOpen ? DropdownUpIcon : DropdownDownIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
<DropdownMenuItem onClick={() => alert("Save as draft")}>
Save as draft
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert("Save and publish")}>
Save and publish
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert("Save a copy")}>
Save a copy
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ButtonGroup>
Size
Set size on the group and every item matches, so the joined shape keeps one height. Button's xLarge is not available here.
<Box className="flex items-center gap-4">
{["small", "standard", "large"].map((size) => (
<ButtonGroup aria-label={`Text formatting ${size}`} key={size} size={size}>
<Button>Cut</Button>
<Button>Copy</Button>
<Button>Paste</Button>
</ButtonGroup>
))}
</Box>
Variant
Set variant on the group and it applies to every item and to the separator colour, so the cluster reads as one control.
<Box className="flex flex-col gap-4">
{[
"accentPrimary",
"accentSecondary",
"neutralSecondary",
"neutralTertiary",
].map((variant) => (
<Box className="flex items-center gap-4" key={variant}>
<Text className="text-body-14 text-secondary w-40">{variant}</Text>
<ButtonGroup aria-label={`Text formatting ${variant}`} variant={variant}>
<Button>Cut</Button>
<Button>Copy</Button>
<Button>Paste</Button>
</ButtonGroup>
</Box>
))}
</Box>
Disabled Item
Items keep the Button and IconButton states. Use isDisabled on the item, not on the group. A disabled item keeps its slot, so the joined shape and the separators are unchanged.
<ButtonGroup aria-label="Row actions">
<Button>Edit</Button>
<Button isDisabled>Delete</Button>
<Button>Share</Button>
</ButtonGroup>
API Reference
ButtonGroup
| Prop | Default | Description |
|---|---|---|
children | _ | ReactNodeItems rendered within the group, normally Button or IconButton. A separator is rendered between every pair of items. Only elements are kept: false, null, strings and numbers are dropped. A component that renders null is still an element, so it still gets a separator; render it conditionally instead. |
size? | 'standard' | 'small' | 'standard' | 'large'Size applied to every item that does not set its own. Button's xLarge is not available. |
variant? | 'neutralSecondary' | 'accentPrimary' | 'accentSecondary' | 'neutralSecondary' | 'neutralTertiary'Visual variant applied to every item that does not set its own, and to the separator colour. |
aria-label? | _ | stringAccessible name for the group. Provide this or aria-labelledby. |
aria-labelledby? | _ | stringId of an existing element that names the group. |
id? | _ | stringUnique identifier for the group root element. |
ButtonGroup sets size and variant on its items through context, so Button and IconButton need no group-specific wrapper. An item that sets either prop itself overrides the group.
Style API
Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.
Please refer to the Style API documentation for more insights.
ButtonGroup parts
Every part below is deliberately over-styled so each one is easy to spot.
<ButtonGroup
aria-label="Text formatting"
className="rounded-[28px] bg-[#fde047] p-1 after:border-2 after:border-dashed after:border-[#a21caf]"
classNames={{
item: "bg-[#ffffff] font-stronger uppercase tracking-[0.15em] text-[#a21caf]",
}}
>
<Button>Cut</Button>
<Button>Copy</Button>
<Button>Paste</Button>
</ButtonGroup>
| Part | Description |
|---|---|
root | Group container, styled through the className and style props rather than classNames. Use it for the joined outline, background, radius, and shadow. |
item | Styles merged into every item. Use it for changes that must stay identical across the group; style a single item through that item's own className. |
An item's own className and style are merged on top of the group's item part, so a one-off item override wins. The separator takes its size and colour from the group's size and variant and is not a styleable part.
Usage guidelines
Do
- Group related actions: Join actions that act on the same object or context, such as the actions for one table row.
- Name the group: Give every group an
aria-labeloraria-labelledby, and every icon item its ownaria-label. - Keep the group short: Three to five items keeps the control scannable and fits small screens.
- Set size and variant on the group: Set them once on
ButtonGrouprather than on each item, so the whole cluster matches.
Don’t
- Don't use it for selection: ButtonGroup holds no selection state. Use SegmentedControl when one item must stay selected.
- Don't mix unrelated actions: Joining actions implies they belong together; keep unrelated actions as separate buttons.
- Don't nest other elements directly: Keep
ButtonandIconButtonas the direct children; other elements take the separators and the joined corners. - Don't place an inline overlay in the group:
PopoverandActionBarrender inline by default, so they become an item and take the end corner; setshouldUsePortalor render the overlay outside the group. - Don't disable the whole group: Disable individual items with
isDisabledinstead.