Button Group
A button group joins a set of related buttons into a single, edge-to-edge control.
Components · Actions
A button group joins a set of related buttons into a single, edge-to-edge control. Built by wrapping nm-button elements in .nm-button-group, it collapses the inner corners and overlaps borders so the buttons read as one segmented unit — for switching a view, paging through results, or clustering commands that belong together.
Overview
A button group takes two or more buttons that share a purpose and binds them into one control. The inner corners square off, adjacent borders overlap into a single hairline, and the whole thing reads as a connected toolbar segment rather than a row of loose chips.
There are two jobs it does well:
- Single-select — a small set of mutually exclusive options where one is always active (a view switcher, a date range, a sort order). The selected button stays visibly pressed.
- Action cluster — a handful of related, independent commands sitting side by side (zoom in / reset / out, undo / redo, alignment).
When to use
- To switch between 2–5 mutually exclusive views or modes (
List/Board/Calendar) - To group tightly related commands in a toolbar (
–/100%/+) - For pagination and stepper controls (
Prev/1/2/3/Next)
When not to use
- For a styled, track-backed single-select with a sliding indicator — use a segmented control instead. A button group is the squarer, more utilitarian sibling.
- For one standalone action — use a plain button.
- When the options need to wrap to multiple lines or carry long labels — use stacked buttons or a list. A group is meant to stay on one line.
- For navigation between unrelated pages — use links or a tab bar.
Anatomy
A button group is a flex container (.nm-button-group) wrapping ordinary .nm-button elements. The group does no styling of its own beyond layout — it overlaps each button's leading edge by -1px so shared borders never double up, then strips the inner corners so only the two end caps stay rounded.
What each part is driven by
| Part | Behaviour | Source |
|---|---|---|
| Group container | display:inline-flex — keeps items on one line | .nm-button-group |
| Shared border | Each item after the first shifts margin-inline-start:-1px so borders collapse to one | .nm-button-group .nm-button:not(:first-child) |
| Inner corners | First item loses its right corners, last loses its left, middles go fully square | :first-child / :not(:first-child):not(:last-child) / :last-child |
| Hover / focus lift | Hovered, focused, or active item gets position:relative; z-index:1 so its full border paints over neighbours | .nm-button-group .nm-button:hover, …:focus-visible, …:active |
| Selected item | Secondary buttons marked aria-pressed="true" / .is-selected take the active fill | .nm-button-group .nm-button--secondary[aria-pressed=true] |
| End cap radius | Outer corners inherit each button's own border-radius (size-dependent) | .nm-button--{size} |
Square & pill
Two end-cap shapes. The default squares the inner corners and keeps the outer corners at the button's own radius. Add .nm-button-group--pill to round the two end caps into a fully capsule-shaped control.
Square (default)
Pill
Pill end caps read softer — reach for them on filter bars and date pickers; keep square for dense editing toolbars.
Single-select
The most common use: a set of mutually exclusive options where exactly one is active at all times. Mark the active button with aria-pressed="true" (or add .is-selected) — the selection styling is wired for secondary buttons, which is the intent a group should almost always use.
Exactly one button carries aria-pressed="true". Selection is a state of the group, not of any single button.
- Keep one — and only one — option selected at all times. A single-select group has no empty state.
- Use secondary intent so the unselected buttons stay quiet and the active one reads clearly against them.
- Order options the way a user thinks about them (chronological, small→large), not alphabetically.
Action groups
When the buttons are independent commands rather than a single choice, drop the pressed state — every button is just a regular button that happens to be joined to its neighbours. Use this for steppers, paging, and command clusters.
Stepper
Pagination
Undo / redo
Sizes
A group inherits its height, padding, font size, and corner radius from the nm-button--{size} class on its children — there is no separate group size. Use the same size on every button in a group; mixing sizes breaks the shared border. The five steps mirror the button scale.
Choosing a size
xs/sm— dense toolbars, table headers, filter rowsmd— default for panels, cards, page-level view switcherslg/xl— marketing surfaces, touch-first kiosks, signing screens
Icon groups
Buttons in a group take the same icon options as standalone buttons: leading icon, trailing icon, or icon-only. Icon-only groups are the workhorse of editing toolbars — keep them to universally understood glyphs and give every button an aria-label.
Icon-only group · md · every button labelled. Match the icon weight across the group so the row reads evenly.
Icon + label
States
Each button in a group keeps its own interaction states. The group adds one rule: a hovered, focused, or active button is lifted with z-index:1 so its full border paints cleanly over the overlapped neighbours — no broken seams.
Secondary · md · default, hover, selected, and active within one group.
Selected vs. active
- Selected (
aria-pressed="true"/.is-selected) is a persistent state — the option the user has chosen. It survives focus moving elsewhere. - Active is the momentary press while the pointer is down. Don't conflate them: a single-select group shows exactly one selected button, and any button can be active for the instant it's clicked.
Focus
Focus uses the button's own :focus-visible ring. Inside a group the focused button lifts above its neighbours so the full ring is visible on every side. Never suppress it.
No disabled state
Like buttons, groups don't ship a disabled appearance. If an option isn't available, leave the rest of the group enabled and explain the constraint inline rather than greying a segment out.
Light & dark theme
Groups inherit every colour from the button tokens, so they remap automatically under data-theme. Fill, selected state, border, and focus ring all adapt — author once.
Both previews are locked to their theme — toggle the page theme to see the rest of the docs cascade.
Groups in context
Where button groups earn their keep. These are layout recommendations, not rigid rules.
List header — view switcher
A quiet secondary group for switching the view, a single loud accent button for the primary action. Don't let the group compete with it.
Filter bar — date range
Editor toolbar
Break a long toolbar into several short groups by function. The gaps between groups do the visual grouping for you.
Do & don't
The rules that keep a group reading as one clean control.
aria-label.Accessibility
A button group is a layout wrapper — semantics live on the buttons and on the role you give the container.
Choosing a role
- Single-select (mutually exclusive, one always active): wrap in
role="group"with anaria-label, and mark each button witharia-pressed="true|false". For a stricter radio-style relationship, userole="radiogroup"on the container androle="radio"+aria-checkedon the buttons. - Action cluster (independent commands): use
role="group"with anaria-labelthat names the cluster ("Pagination","Text formatting"). Each button stays a normal<button type="button">.
Keyboard
- Tab moves into the group and Tab moves out — every button is individually focusable by default.
- Space and Enter trigger the focused button.
- For
role="radiogroup", manage arrow-key roving focus so the group is a single Tab stop, per the WAI-ARIA radio pattern.
Labeling
- Always give the container an
aria-label(oraria-labelledby) so assistive tech announces what the group controls. - Icon-only buttons each require their own
aria-labeldescribing the action, not the glyph ("Align left", not"left icon"). - Reflect selection with
aria-pressed(toggle buttons) oraria-checked(radio pattern) — never rely on the fill colour alone.
Contrast
- Selected and unselected segments both meet WCAG 2.2 AA for label-over-fill in light and dark themes.
- The focus ring keeps ≥ 3:1 contrast against the button and the page, and lifts above neighbours so it's never clipped.
Playground
Click a segment to select it. The markup updates live — copy it straight into your product.
Token reference
A button group has no colour tokens of its own — every fill, border, and selected state resolves through the button tokens. The group layer only owns layout and corner behaviour.
Inherited from button
| Token | Role |
|---|---|
--nm-button-secondary-default-bg | Segment fill at rest |
--nm-button-secondary-active-bg | Fill of the selected segment (aria-pressed / .is-selected) |
--nm-button-secondary-default-border | Shared segment border |
--nm-button-secondary-fg | Label + icon colour |
--palette-focus-700 / --palette-focus-300 | Focus ring (light / dark) |
Group-layer behaviour (no custom properties)
| Aspect | Value | Source |
|---|---|---|
| Layout | display:inline-flex | .nm-button-group |
| Border overlap | margin-inline-start:-1px on every item after the first | .nm-button-group .nm-button:not(:first-child) |
| Inner corners | squared; outer corners keep the button's own radius | first / middle / last child rules |
| Pill end caps | border-radius:999px on the two outer corners | .nm-button-group--pill |
| Hover / focus / active lift | position:relative; z-index:1 | .nm-button-group .nm-button:hover, …:focus-visible, …:active |
Geometry comes entirely from the nm-button--{size} class on the children — there's nothing to override on the group itself.
Implementation
Wrap a row of nm-button elements in .nm-button-group. No JavaScript is required for layout; you only wire up click handling and the aria-pressed toggle for single-select behaviour.
<link rel="stylesheet" href="namirial-theme.css">
<div class="nm-button-group" role="group" aria-label="View">
<button class="nm-button nm-button--secondary nm-button--md" aria-pressed="true">List</button>
<button class="nm-button nm-button--secondary nm-button--md" aria-pressed="false">Board</button>
<button class="nm-button nm-button--secondary nm-button--md" aria-pressed="false">Calendar</button>
</div>CSS classes
| Class | Applies to | Effect |
|---|---|---|
nm-button-group | container | Joins children edge-to-edge, collapses inner borders & corners |
nm-button-group--pill | container | Rounds the two end caps into a capsule |
nm-button--secondary | each button | The intent groups should use; wires up the selected fill |
nm-button--{size} | each button | Sets height, padding, font size, radius — keep identical across the group |
aria-pressed="true" / is-selected | one button | Marks the selected segment in a single-select group |