Button group
Joins related buttons into one control.
Orientation
horizontaldefault
vertical
Content
textdefault
separator
split
States
rest
hover
held
focus-visible
disabled
Usage
When to use it
- Use
- Two or three actions of one kind that belong together: previous and next, zoom out and in, pause and resume.
- Use
- A value between the buttons that change it (ButtonGroupText).
- Use
- A row of quiet ghost actions split into halves (ButtonGroupSeparator).
- Use
- One action with a few close alternatives behind a caret (ButtonGroupMenu, the split button).
- Not for
- One view of several is chosen. Use Segmented control.
- Not for
- Unrelated actions that only share a row; space them 8px apart. Use Button.
Rules
7 rules
- Must
- Make every member variant="secondary" or "ghost"; put the one slab beside the group, not in it. Because the group is one control, and a yellow cell joined to edged ones reads as a chosen segment rather than the next step.
- Must
- Give every member the same size and variant. Because mixed heights break the seams, and mixed surfaces make one cell look chosen.
- Must
- Name the group with aria-label when its members do not say what they act on. Because "Previous, Next" means nothing without the thing they page through.
- Never
- Show a chosen member by giving it the slab or the selection wash; that is a segmented control. Because every member of a group is an action, and a member styled as chosen reads as a view switcher with no radio semantics.
- Never
- Add a gap or margins between members. Because the members share one 1px seam, and a gap turns the one control into loose buttons with doubled edges.
- Should
- Keep a group to two or three members, plus a ButtonGroupText. Because a long joined row is hard to aim at and reads as a segmented control.
- Should
- Use ButtonGroupText for the value the buttons change, and announce it with aria-live="polite".
State order
5 states, highest first
- 1
- disabled
- 2
- held
- 3
- focus-visible
- 4
- hover
- 5
- rest
Accessibility
4 checks
- role="group" named by aria-label; each member is its own button in the tab order.
- Icon members need aria-label; ButtonGroupText with aria-live announces the new value.
- The focused member rises over its neighbours and draws its 2px ring on its own edge, so the ring shows whole.
- ButtonGroupText is text-2 on the page, 7.8:1 and 10.2:1.
Keyboard
2 keys
- Tab
- Moves from member to member.
- Enter, Space
- Presses the focused member.
Edge cases
7 edge cases
- Member held
- The fill steps darker and nothing scales, so the seams never open.
- Member disabled
- Drops to the disabled label in place; the seams stay.
- Group of groups
- Groups nested in a ButtonGroup sit 8px apart, each its own ruled row.
- Right to left
- Members run right to left and the seams follow.
- Dark theme
- Members sit on the near-black page; their --line edges make the seams.
- Split button
- The caret's menu lines up under the whole group and is at least as wide; the caret turns at once while it is open.
- Split button while running
- Pass the main action's pending to ButtonGroupMenu, so the alternatives lock with it.
API
6 props
orientation'horizontal' | 'vertical'default'horizontal'aria-labelstringButtonGroupTextdiv (render)ButtonGroupMenu label, size, variantstring, sm | default | lg, secondary | ghostdefault'default', 'secondary'ButtonGroupMenu pendingbooleanButtonGroupSeparator orientation'vertical' | 'horizontal'default'vertical'
Tokens
7 tokens
--line- #e2dfd5
--secondary-bg- #f8f7f3
--text-2- #55524a
--control- 32px
--z-raised- 10
--type-label- 400 13px/16px var(--font-mono)
--focus- #7a5d00