Skip to content
General

Button Group

Buttons welded into one unit: the seams collapse to a single 2px rule and only the outer corners stay rounded. Styling only — the buttons keep their own handlers and their own place in the tab order.

Import

import { ButtonGroup } from "@mikenotthepope/substrateui"

How the seams close

The group is a flex row with -space-x-[2px], pulling each button back over its neighbour's border so two 2px rules read as one. Then every child except the first loses its leading corners and every child except the last loses its trailing ones — rounded-s-none and rounded-e-none, logical properties, so the shape is right in RTL without a second rule.

That is done by cloning each child with an added className. A child that ignores className keeps all four corners and the group looks like separate buttons overlapping. Wrap such a child in an element that does accept one.

A split button

The common shape: a primary action, and a menu trigger welded to its end. Give the trigger an aria-label — a lone chevron has no accessible name.

Icon groups

Icon-only buttons group tightly into a toolbar. If the buttons are a set of states rather than a set of actions — one of which is currently on — you want ToggleGroup instead, which adds the pressed state and arrow-key navigation.

Accessibility

The group adds no roles and no keyboard handling. Each button is its own tab stop, which is right for a row of independent actions and wrong for a set of alternatives — for that, ToggleGroup makes the whole set one tab stop with arrow keys inside it.

The visual welding is the only thing saying these buttons belong together, and that cue is not in the accessibility tree. Where the grouping carries meaning, wrap it in a role="group" with an aria-label.

Focus rings still work, and the ring is drawn with an offset, so the focused button lifts clear of its neighbours rather than being clipped by the overlap.

API Reference

ButtonGroup defines no props of its own beyond what it does to its children — everything else, including className, goes to the wrapping div. See React.HTMLAttributes<HTMLDivElement> for that surface.

PropTypeDefaultDescription
childrenReact.ReactNodeThe buttons. Each element child is cloned with the corner-rounding classes it needs for its position, so children must accept a className.