Button group

Joins a few related actions into one control.

Examples

An action with more options

A stepper

3 agents

Orientation

  • horizontaldefault

  • vertical

Content

  • buttons

  • 2 of 9

    with text

  • with menu

  • with separator

States

  • rest

  • hover

  • focus

  • disabled

  • menu open

  • pending

Usage

When to use it
Use
Two to four actions work on the same thing: zoom out, fit, zoom in.
Use
One main action has close alternatives most people never need: Send reply, plus Send and close.
Use
A value sits between two buttons that change it: previous page, 2 of 9, next page.
Not for
The person picks one option and it stays picked. Use Segmented control.
Not for
The actions are unrelated and only share a row. Use Button.
Not for
More than four actions, or alternatives that are not variants of one action. Use Dropdown menu.

Rules

9 rules
Must
Pass the main action pending to ButtonGroupMenu so the caret locks while it runs. Because an open caret mid-run lets the person start a second variant of the same reply.
Must
Give every member the same variant and size. Because the shared line and the 1px paper split only read as one control when the fills match, and mixed heights break the squared inner corners.
Should
Keep text members to three in a 320px column; move the rest into a ButtonGroupMenu. Because the group is w-fit and never wraps, so a fourth label pushes it past a phone column.
Must
Set the menu label to what the alternatives are, such as "More send options". Because label is the caret's only accessible name, and "More" does not say more of what.
Should
Start each menu item with the main verb and describe what differs.
Must
Pass focusableWhenDisabled on a member that disables at a limit and announce the value in a status line. Because a plain disabled button drops focus to the page body when the count hits its limit.
Must
Name the group with aria-label when its members do not say what they share. Because ButtonGroup sets role="group", and a screen reader announces an unnamed group as just "group".
Never
Add a border to each member; the group draws one shared line, and ButtonGroupSeparator divides ghost members. Because a border doubles where two members meet and grows the member, so the group no longer lines up with a lone button.
Never
Build view switching from a ButtonGroup and a highlighted member; use SegmentedControl. Because a highlighted Button has no pressed or selected state, so a screen reader cannot tell which view is on.

Accessibility

6 checks
The root is role="group"; Tab moves between members, arrows do not.
The caret is a menu button with aria-haspopup and aria-expanded, named by label.
Name the group when the members do not say what they share.
A member that disables at a limit keeps focus with focusableWhenDisabled.
The focused member sits above its neighbours so the ring stays whole.
Escape closes the menu and returns focus to the caret.

Edge cases

11 edge cases
Long member label
Labels never wrap, so the group widens; the menu caps at --popover and wraps descriptions.
Icon-sm members
28px cells sharing one control line.
No items in the menu
Opens an empty popup; do not render ButtonGroupMenu with no items.
Four members in 320px
The joined row does not wrap and overflows; the menu shifts 8px inside the viewport.
Right to left
Corners are logical; the group mirrors whole.
Zoom 200%
Members stretch to the tallest; the joined shape holds.
Main action pending
The caret disables too and the box keeps its width.
Failed alternative
Report beside the group; the group has no error state.
Escape in a menu inside a dialog
Closes only the menu.
Forced colors
Each member draws a ButtonText border; the separator paints CanvasText.
Reduced motion
The caret turns without a transition; the menu appears at once.

API

9 props
ButtonGroup orientation
'horizontal' | 'vertical' default 'horizontal'
ButtonGroup aria-label
string
ButtonGroupMenu label
string default required
ButtonGroupMenu variant, size
'primary' | 'secondary' | 'ghost', 'sm' | 'default' | 'lg' default 'primary', 'default'
ButtonGroupMenu pending
boolean
ButtonGroupMenu align
'start' | 'end' default 'end'
ButtonGroupMenuItem description
ReactNode
ButtonGroupMenuItem variant
'default' | 'destructive' default 'default'
ButtonGroupSeparator orientation
'vertical' | 'horizontal' default 'vertical'

Tokens

16 tokens
--space-2
8px
--line-control
oklch(var(--ink-c) / 0.16)
--line-row
oklch(var(--ink-c) / 0.08)
--surface-page
var(--paper)
--text-2
oklch(var(--ink-c) / 0.80)
--type-label
400 14px/16px var(--font-sans)
--radius-soft
6px
--line-hair
1px
--paper
oklch(var(--paper-c))
--elev-overlay
0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--popover
288px
--dur-base
160ms
--dur-fast
120ms
--ease-step
cubic-bezier(0.2, 0, 0, 1)
--surface-hover
oklch(var(--ink-c) / 0.05)
--fail-text
oklch(51% 0.180 26)

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.