Button Group
Use a Button Group when several controls answer the same question — the actions
on a message, a split button, a value with its unit. The group fuses its direct
children into one object: inner corners squared off, a single hairline between
neighbours, outer corners kept. Renders a <div role="group">.
The children stay ordinary components. A Button keeps
every variant, tone and size; an Input keeps its props;
the group never reaches past their shared edge. And it is purely visual and
semantic — there is no keyboard behaviour, so every control keeps its own tab
stop. When the set should cost one Tab press instead, reach for
Toolbar.
Import
import { ButtonGroup } from "@forte-ui/react";<ButtonGroup.Root aria-label="Message actions">
<Button variant="outline" tone="neutral">Archive</Button>
<Button variant="outline" tone="neutral">Report</Button>
<Button variant="outline" tone="neutral">Snooze</Button>
</ButtonGroup.Root>Examples
Orientation
orientation="vertical" stacks the controls and fuses the horizontal edges
instead. The column sizes to its widest control and stretches the narrower ones
up to it, which is what makes the stack read as one object.
There is no size prop on the group, deliberately: the controls carry their own
size, and fusing does not require them to agree — though a group whose
children are three different heights stops reading as one object, so in
practice you want them to.
Separators
A fused pair shares one hairline, and where does it come from? From the
children: every forte-ui control draws a 1px border of its own — transparent
in the fill variants — and the group strips the shared edge from the second of
the pair so the first one's border is the seam. With outline children that
seam is visible and separators are unnecessary. With solid or soft
children the seam is transparent, the group reads as one unbroken slab, and
ButtonGroup.Separator is what puts the boundaries back.
Split button
The commonest fused pair: the action, and a menu of its variations. The
separator's default colour is tuned against the page background, so between two
solid fills re-point --forte-button-group-separator-color towards the fill's
own text colour, as the demo does.
With an input
An Input fuses like any other control — it draws the same 1px border a button
does. ButtonGroup.Text is the non-interactive cell for context around it: a
unit, a prefix, a count. It has no height of its own on purpose — the group
stretches it to match whatever control it sits beside, so one cell serves every
size.
Render the cell as a <label> when it names the field beside it, so clicking
the prefix drops the caret into the input:
<ButtonGroup.Text render={<label htmlFor="amount" />}>USD</ButtonGroup.Text>
<Input id="amount" />Nested groups
A group whose direct children are themselves groups switches from fusing to
spacing: each inner group fuses its own controls, and the outer one lays them
out with a gap — one object per decision, one gap between decisions. The gap is
--forte-button-group-gap.
Accessibility
role="group" has no name of its own. Give the group aria-label, or point
aria-labelledby at your own heading, whenever the grouping carries meaning —
"Message actions", "Pagination". Without a name the grouping is invisible to
assistive technology and the controls read as a loose row.
Everything else belongs to the children, and stays theirs: each control keeps its own tab stop, its own focus ring, its own disabled and loading semantics. The group's one intervention is visual — a focused control rises above the seam so its ring is never clipped by the neighbour painted after it.
Theming
| Property | Controls | Default |
|---|---|---|
--forte-button-group-gap | Gap between nested groups. Applies only when the group's direct children are themselves groups — a fused group has no gaps. | var(--forte-space-2) |
The controls inside keep their own properties — --forte-button-* on
Button, --forte-input-* on
Input. The group's own surface is nothing: every visible
pixel belongs to a child.
Separator
| Property | Controls | Default |
|---|---|---|
--forte-button-group-separator-thickness | Line thickness. Matches the 1px border every control draws, so a separated seam and a fused one carry the same weight. | 1px |
--forte-button-group-separator-color | Line colour. Re-point it when the separator sits between solid fills — --forte-color-border is tuned against the page background, not against a colour fill. | var(--forte-color-border) |
The line is painted with a border, never a background-color. A background
is replaced with a system colour under forced colours, so a hairline drawn that
way vanishes for exactly the users who most need the boundary; border colours
are forced to CanvasText and survive.
Text
| Property | Controls | Default |
|---|---|---|
--forte-button-group-text-bg | Fill behind the text. | var(--forte-color-panel) |
--forte-button-group-text-color | Text colour. Muted on purpose — the cell is context for the action beside it, not the action. | var(--forte-color-foreground-muted) |
--forte-button-group-text-border-color | Border colour. | var(--forte-color-border) |
--forte-button-group-text-radius | Corner radius. The group squares off whichever corners are fused. | var(--forte-radius-control) |
--forte-button-group-text-padding-x | Horizontal padding. | var(--forte-control-px-md) |
--forte-button-group-text-font-size | Label size. | var(--forte-font-size-2) |
API reference
ButtonGroup.Root
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
orientation | ButtonGroupOrientation | horizontal | Which way the controls fuse — a row or a column. Layout only: unlike [`Toolbar`](/components/toolbar), a button group adds no keyboard behaviour, so there is no arrow-key axis to move. |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<div>` with another element or component. |
ButtonGroup.Separator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
orientation | ButtonGroupOrientation | Which way the line runs. Defaults to the *opposite* of the group's orientation — a row gets vertical seams — which is almost always what you want, so leave it unset. |
ButtonGroup.Text
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<div>` with another element or component — pass `render={<label htmlFor="…" />}` when the cell names the input fused beside it, so clicking it focuses the field. |