Form actions
Overview
The Button group sits between two or more <Button> children and gives them a consistent gap, direction, and alignment. It does not collapse borders or share a focus ring — each button keeps its own shape, variant, and state. Reach for it whenever buttons live next to each other; never hand-roll the layout with margins.
Not a segmented control
A segmented control merges adjacent buttons into one shape with shared corners and a single chosen state. Flatpay's button group keeps the buttons distinct — that's the design. If you need a segmented picker, reach for the upcoming SegmentedControl component instead.
Compositions
Four canonical patterns cover almost every screen we ship. Reach for the one that fits the surface — don't invent a fifth.
Form actions
Cancel + Primary
Standard footer for forms and dialogs. Cancel sits on the left, the primary action sits on the right of the row — closest to the user's reading exit.
Toolbar
Filter + Action + Action
Adjacent toolbar actions of equal weight. Use secondary or tertiary variants — never two primaries in a single bar.
Row actions
Action + Overflow
A single visible action plus an overflow menu for the rest. Common in tables, list rows, and prototype cards.
Stretch fill
Two equal buttons
Each child fills 1fr — a common shape on mobile bottom sheets and dialogs where width is precious. Use sparingly on desktop; equal-width buttons cancel hierarchy.
Orientation
Two directions. Default is horizontal — the buttons share a baseline. Vertical stacks them; reach for it on narrow surfaces only.
orientation="horizontal"
The default. Use for form footers, toolbar strips, and any context where the buttons share a baseline.
orientation="vertical"
Stacks children. Reach for it on narrow surfaces — mobile bottom sheets, sidebar action lists, narrow card footers.
Alignment
Four options. Default is start; reach for between when a back link and a forward action need the full row width; stretch only on narrow widths where buttons must fill 1fr each.
align="start"
Default. Pushes the group to the leading edge of its container.
align="end"
Pushes the group to the trailing edge — used in form footers when the action sits opposite a back link.
align="between"
Pushes the first child to the leading edge and the last to the trailing edge — ideal for Cancel ↔ Save dialogs.
align="stretch"
Each child fills 1fr. Reach for it on narrow widths (mobile, dialogs) only.
Spacing
Three gap sizes. Default is md (8 px) — the same gap used between siblings throughout the system.
gap="sm"
6 pxTight clusters — row actions, dense toolbars, table cells.
gap="md"
8 pxThe default. Form footers, dialog actions, top-level toolbars.
gap="lg"
12 pxGenerous separation — splash screens, marketing surfaces, hero CTAs.
Anatomy
Three named parts. The wrapper is a role="group" container; the children remain individual buttons; the gap is owned by the wrapper, not the buttons.
Wrapper
Container with
role="group". Owns direction (orientation), spacing (gap), and positioning (align).Children
Individual
<Button>components — each keeps its own variant, size, and focus ring. Mix variants freely (one primary, several secondaries) but never two primaries.Gap
CSS
gapon the wrapper. Default 8 px — sits on the system's 4-pixel grid. Children carry no horizontal margin.
Behavior
- No shared state. The group is layout-only. Each child is its own button, with its own click, loading, and disabled handling.
- Tab order follows the DOM. In
orientation="horizontal", that means left-to-right; in vertical, top-to-bottom. Place the most likely next action last in the DOM so it earns the final tab stop before the user moves on. - Wrapping is opt-in.
wrap={true}lets the group break to a new line when the container narrows. By default it stays on one row — overflow becomes a clipping problem you should fix with composition (drop a button into an overflow menu) rather than wrapping. - Stretch is responsive, not decorative. Use
align="stretch"when width is constrained — mobile bottom sheets, narrow dialogs. On desktop, equal-width buttons cancel hierarchy.
Accessibility
- Set
labelon every group that isn't already named by an adjacent heading. The prop emitsaria-labelon therole="group"wrapper — screen readers announce it once before reading the children. - Don't mix link and button semantics inside one group. A "View profile" link next to a "Delete" button reads as ambiguous — split them into two patterns or render the link as a button styled like a tertiary action.
- Mobile: prefer vertical orientation on narrow widths. Three buttons in a row reach below the WCAG 24-px target spacing on most phones. Either stack vertically or reduce to two children + an overflow menu.
- Focus ring inheritance.Each child carries its own focus ring; the group does not. Don't add an outer focus outline to the wrapper — it competes with the children's rings.
Code
Import alongside Button. The wrapper takes a label for screen readers and a few layout props; everything else stays on the children.
tsx
import { Button, ButtonGroup } from "@flatpay-dk/ui";
// Default — Cancel + primary
<ButtonGroup label="Save or discard">
<Button variant="tertiary">Cancel</Button>
<Button variant="primary">Save changes</Button>
</ButtonGroup>
// Toolbar — three secondaries, tighter gap
<ButtonGroup label="Catalog toolbar" gap="sm">
<Button variant="tertiary" leadingIcon={<FilterIcon />}>Filter</Button>
<Button variant="tertiary" leadingIcon={<DownloadIcon />}>Export</Button>
<Button variant="tertiary">Sort</Button>
</ButtonGroup>
// Row actions — primary action + overflow menu
<ButtonGroup label="Row actions" gap="sm">
<Button variant="tertiary" size="sm">Edit</Button>
<Button variant="tertiary" size="sm" iconOnly aria-label="More actions">
<MoreIcon />
</Button>
</ButtonGroup>
// Stretch — used in mobile bottom sheets
<ButtonGroup align="stretch" label="Confirm">
<Button variant="tertiary">Cancel</Button>
<Button variant="primary">Confirm</Button>
</ButtonGroup>
// Vertical stack — narrow contexts
<ButtonGroup orientation="vertical" label="Mobile actions">
<Button variant="primary">Confirm</Button>
<Button variant="secondary">Save draft</Button>
<Button variant="tertiary">Cancel</Button>
</ButtonGroup>Best practices
The group is just layout, but the *composition* inside it carries weight. A few rules keep that weight readable.
Do
One primary per group. Cancel sits left, primary sits right (closest to the reading exit).
Don't
Don't pair two primaries. The user reads neither as the next step.
Do
Stretch on narrow widths. Two equal buttons fill 1fr each; the dialog reads like a clean fork.
Don't
Don't stretch on desktop. Equal-width buttons in a wide row cancel hierarchy.
Do
Move the rest into an overflow menu when a row would have four or more actions.
Don't
Don't crowd the row with five visible actions. Density isn't the same as legibility.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | ARIA label describing the group's purpose. Required when the group isn't already named by an adjacent heading. |
| orientation | "horizontal" | "vertical" | "horizontal" | Layout direction. Vertical stacks children full-width; horizontal sits them on a shared baseline. |
| gap | "sm" | "md" | "lg" | "md" | Distance between children. sm=6 px · md=8 px · lg=12 px. |
| align | "start" | "end" | "between" | "stretch" | "start" | Justification along the main axis. start/end push to one edge; between fills the row; stretch makes each child fill 1fr. |
| wrap | boolean | false | Allow children to wrap to the next line when the container narrows. Off by default — overflow should usually be solved by composition, not wrapping. |
| ...rest | HTMLAttributes<HTMLDivElement> | — | All standard div attributes (className, id, data-*, etc.) pass through to the wrapper. |