Button group

Joins related buttons into one control.

Orientation

  • horizontaldefault

  • vertical

Split

  • menudefault

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, fewer and more, zoom out and in.
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, a 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 ink action beside the group, not in it. Because the group is one control, and an ink cell in a flat pill reads as a second lead next to the real one.
Must
Give every member the same size and variant. Because mixed heights break the seams, and mixed keys 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 making it ink or pressed-looking; 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.
Must
Make a split button with ButtonGroupMenu as the last member, matching the main action's variant and size. Because a separate DropdownMenu beside the group opens under the caret alone and reads as a second control, and a mismatched caret breaks the seam.
Should
Keep a group to two or three members, plus a ButtonGroupText. Because a long joined pill 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

3 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 so its 2px ink ring shows whole.

Keyboard

2 keys
Tab
Moves from member to member.
Enter, Space
Presses the focused member.

Edge cases

7 edge cases
Member held
No press scale, so the seams never open.
Member disabled
Turns flat grey in place; the seams stay.
Group of groups
Groups nested in a ButtonGroup sit 8px apart, each its own pill.
Split button open
The menu opens under the whole group, at least as wide as it, aligned to its end.
Split button while the action runs
Pass pending to ButtonGroupMenu; the caret locks so a second variant cannot start.
Right to left
The rounded ends follow the reading direction.
Dark theme
Paper members turn near-black; their rings make the seams.

API

6 props
orientation
'horizontal' | 'vertical' default 'horizontal'
aria-label
string
ButtonGroupText
div (render)
ButtonGroupMenu label, variant, size
string, 'secondary' | 'ghost', 'sm' | 'default' | 'lg' default , 'secondary', 'default'
ButtonGroupMenu pending, align
boolean, 'start' | 'end' default false, 'end'
ButtonGroupSeparator orientation
'vertical' | 'horizontal' default 'vertical'

Tokens

9 tokens
--flat-ring
var(--line-faint)
--flat-bg
var(--paper)
--line
rgb(5 5 5 / 0.10)
--text-2
rgb(5 5 5 / 0.60)
--radius-pill
999px
--radius-overlay
10px
--control
32px
--z-raised
10
--type-label
500 13px/1.35 var(--font-sans)

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.