ButtonGroup
ImplementedGroup of related buttons with role=group semantics. Declarative items mode (array of ButtonGroupItem with optional icon + hidden) or slot mode (caller-provided children). Aligns start/end, wraps responsively.
patterns/molecules/buttongroup.htmlGroup of related buttons with role="group" semantics. Declarative items mode (array of ButtonGroupItem with optional icon + hidden) or slot mode (caller-provided children). Aligns start/end, wraps responsively.
ButtonGroup
Content & Layout molecule (Family 6)
Renders a role="group" row of Button atoms — declarative items mode or slot mode (caller-provided children). Items and children are mutually exclusive: when both are given, items win.
Items mode — declarative actions
Click a button — each item carries its own onClick handler.
Hidden item — permission-gated (DOM omission)
Delete is hidden: not rendered at all — never reachable by Tab or a screen reader.
Slot mode — caller-provided children
align="end" — pushed right inside a flex parent
This component supports light and dark mode.
Usage
ButtonGroup is the DS molecule for a row of related actions: it wraps the buttons in role="group" semantics with a required accessible name, composes the Button atom (variants, sizes, loading, disabled, 44px tap floor), and wraps responsively on narrow viewports. Use it instead of a hand-rolled flex gap-2 div of Buttons — the group semantics and label are the point.
import { ButtonGroup, Button } from '@epasslive/design-system'
// Declarative items mode
<ButtonGroup
ariaLabel="Quick actions"
items={[
{ id: 'pdf', label: 'Download PDF', variant: 'primary', onClick: downloadPdf },
{ id: 'wallet', label: 'Add to Apple Wallet', variant: 'secondary' },
{ id: 'share', label: 'Share link', variant: 'ghost' },
]}
/>
// Slot mode — caller-provided children
<ButtonGroup ariaLabel="Edit actions">
<Button variant="secondary" size="sm">Cancel</Button>
<Button size="sm">Save</Button>
</ButtonGroup>
// Permission-gated item — hidden is omitted from the DOM entirely
<ButtonGroup
ariaLabel="Admin actions"
items={[
{ id: 'edit', label: 'Edit', variant: 'primary' },
{ id: 'delete', label: 'Delete', variant: 'danger', hidden: !canDelete },
{ id: 'export', label: 'Export CSV', variant: 'secondary' },
]}
/>Two modes, one contract
- items mode — declarative: one source-of-truth array, Button atoms rendered in order, per-item variant/size/loading/disabled/icon/hidden. Best for action clusters driven by data or permissions.
- slot mode (children) — wrap your own Buttons (or mixed controls, e.g.
<Select>+ Buttons) in the group semantics. Best for form footers where buttons carry non-declarative props (type="submit", refs). - mutual exclusivity — when both
itemsandchildrenare given,itemswins.
Alignment
align="end" applies ml-auto to the group container — it only works when the parent is a flex container (the auto margin then pushes the group to the end of the main axis). Inside a block-level parent the ml-auto is inert and the group stays left-aligned. All current align="end" consumers sit in flex/toolbar parents.
Props
ariaLabel*stringAccessible name for the group (WAI-ARIA 1.2). Required — the container renders role="group" and must be named.
itemsButtonGroupItem[]Declarative mode: renders Button atoms in order. Each item: id, label, icon?, variant?, size?, loading?, disabled?, hidden?, onClick?, ariaLabel?. See the ButtonGroupItem table below.
childrenReact.ReactNodeSlot mode: wraps caller-provided Buttons (or mixed controls) in the group semantics. Use when buttons need non-declarative props (form submission, refs). Items wins when both are given.
align'start' | 'end''start'Horizontal alignment. end = ml-auto on the container — only effective inside a flex parent (inert in a block container).
classNamestringExtra classes merged onto the group container (flex flex-wrap items-center gap-3). Note: consumer gap-* utilities lose to the hardcoded gap-3 in the stylesheet order.
idstringStable id for the container. Defaults to a React.useId()-generated id (SSR-safe).
ButtonGroupItem
Fields of each entry in the items array (items mode):
id*stringStable React key.
label*stringVisible button text (the accessible name unless ariaLabel overrides it).
iconReact.ReactNodeLeading icon — wrapped in an aria-hidden span (decorative, excluded from the a11y tree).
variant'primary' | 'secondary' | 'ghost' | 'danger'Button defaultPer-item Button atom variant.
size'sm' | 'md' | 'lg'Button defaultPer-item Button atom size. All sizes keep the 44px tap floor.
loadingbooleanfalseLoading state — sets aria-busy and disables interaction (Button atom).
disabledbooleanfalseDisabled state (native disabled attribute).
hiddenbooleanfalseVisually omit this item (permission-gated actions, feature flags) while keeping the declarative list stable. Omitted from the DOM entirely, not clipped.
onClick() => voidClick handler for the rendered Button.
ariaLabelstringPer-button accessible name override — takes precedence over label.
Accessibility
- Container renders
role="group"with anaria-label— theariaLabelprop is required by the type system (WAI-ARIA 1.2: a group must be named). - Natural Tab order — every button is individually tabbable. This is the WAI-ARIA group pattern, not a toolbar: arrow-key roving belongs to toolbar/segmented patterns (see the dedicated
TableToolbar/TextFormatToolbarmolecules). hiddenitems are omitted from the DOM entirely — not visually clipped and notaria-hidden— so neither Tab nor screen readers ever encounter them (WCAG 2.1.1).- Per-item
ariaLabeloverrides the accessible name when the visible label needs more context; leading icons arearia-hiddendecoration. - Focus-visible ring (2px teal) and the 44px tap floor are inherited from the Button atom (WCAG 2.4.7 / 2.5.5).
Props
ButtonGroup
ariaLabelstring—Accessible name for the group (WAI-ARIA 1.2).
itemsButtonGroupItem[]—Declarative mode: renders Button atoms in order. Each item: id, label, icon?, variant?, size?, loading?, disabled?, hidden?, onClick?, ariaLabel?.
childrenReact.ReactNode—Slot mode: wraps caller-provided Buttons. Items wins when both are given.
align'start' | 'end''start'Horizontal alignment (end = ml-auto for right-aligned action rows).
classNamestring—Additional CSS classes on the group container.
idstring—Root element id.
<ButtonGroup />Accessibility
ARIA
1 ruleKeyboard
1 ruleUsage
Import
import { ButtonGroup } from "@epasslive/design-system"Example 1
<ButtonGroup ariaLabel="Quick actions" items={items} />Example 2
<ButtonGroup ariaLabel="Edit actions"><Button>Save</Button></ButtonGroup>Anti-patterns
— Do not hardcode data in molecules
— Avoid complex business logic in molecules