Skip to content

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">.

button-group/basic.tsx

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.

button-group/orientation.tsx

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.

button-group/separator.tsx

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.

button-group/split.tsx

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.

button-group/input.tsx

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.

button-group/nested.tsx

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

Theming tokens for ButtonGroup
PropertyControlsDefault
--forte-button-group-gapGap 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

Theming tokens for ButtonGroup
PropertyControlsDefault
--forte-button-group-separator-thicknessLine 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-colorLine 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

Theming tokens for ButtonGroup
PropertyControlsDefault
--forte-button-group-text-bgFill behind the text.var(--forte-color-panel)
--forte-button-group-text-colorText 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-colorBorder colour.var(--forte-color-border)
--forte-button-group-text-radiusCorner radius. The group squares off whichever corners are fused.var(--forte-radius-control)
--forte-button-group-text-padding-xHorizontal padding.var(--forte-control-px-md)
--forte-button-group-text-font-sizeLabel size.var(--forte-font-size-2)

API reference

ButtonGroup.Root

Props for ButtonGroupRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
orientationButtonGroupOrientationhorizontalWhich 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.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<div>` with another element or component.

ButtonGroup.Separator

Props for ButtonGroupSeparator
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
orientationButtonGroupOrientationWhich 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

Props for ButtonGroupText
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
renderRenderProp<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.