Actions

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

PartBehaviourSource
Group containerdisplay:inline-flex — keeps items on one line.nm-button-group
Shared borderEach item after the first shifts margin-inline-start:-1px so borders collapse to one.nm-button-group .nm-button:not(:first-child)
Inner cornersFirst 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 liftHovered, 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 itemSecondary buttons marked aria-pressed="true" / .is-selected take the active fill.nm-button-group .nm-button--secondary[aria-pressed=true]
End cap radiusOuter 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.

xs
h 24px · pad 8 · r 6
sm
h 32px · pad 12 · r 8
md
h 40px · pad 16 · r 10
lg
h 48px · pad 24 · r 12
xl
h 56px · pad 24 · r 12

Choosing a size

  • xs / sm — dense toolbars, table headers, filter rows
  • md — default for panels, cards, page-level view switchers
  • lg / 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.

Light data-theme="light"
Dark data-theme="dark"

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

Documents

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.

✓Do
Use secondary intent across the whole group and let one selected segment carry the emphasis.
✕Don't
Don't mix loud intents inside a group. Members of a set should look like siblings, not rivals.
✓Do
Keep groups to 2–5 short, parallel options that fit on one line.
✕Don't
Don't pack long labels or six-plus options into a group — it wraps and the connected look falls apart. Use a select or tabs.
✓Do
In a single-select group, always keep exactly one option selected.
✕Don't
Don't leave a single-select group with nothing chosen — the user can't tell which mode they're in.
✓Do
Give every icon-only button in the group an aria-label.
✕Don't
Don't mix sizes within a group. Different heights break the shared border and the unit stops reading as one.

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 an aria-label, and mark each button with aria-pressed="true|false". For a stricter radio-style relationship, use role="radiogroup" on the container and role="radio" + aria-checked on the buttons.
  • Action cluster (independent commands): use role="group" with an aria-label that 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 (or aria-labelledby) so assistive tech announces what the group controls.
  • Icon-only buttons each require their own aria-label describing the action, not the glyph ("Align left", not "left icon").
  • Reflect selection with aria-pressed (toggle buttons) or aria-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.

<div class="nm-button-group" role="group"> <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>

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

TokenRole
--nm-button-secondary-default-bgSegment fill at rest
--nm-button-secondary-active-bgFill of the selected segment (aria-pressed / .is-selected)
--nm-button-secondary-default-borderShared segment border
--nm-button-secondary-fgLabel + icon colour
--palette-focus-700 / --palette-focus-300Focus ring (light / dark)

Group-layer behaviour (no custom properties)

AspectValueSource
Layoutdisplay:inline-flex.nm-button-group
Border overlapmargin-inline-start:-1px on every item after the first.nm-button-group .nm-button:not(:first-child)
Inner cornerssquared; outer corners keep the button's own radiusfirst / middle / last child rules
Pill end capsborder-radius:999px on the two outer corners.nm-button-group--pill
Hover / focus / active liftposition: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

ClassApplies toEffect
nm-button-groupcontainerJoins children edge-to-edge, collapses inner borders & corners
nm-button-group--pillcontainerRounds the two end caps into a capsule
nm-button--secondaryeach buttonThe intent groups should use; wires up the selected fill
nm-button--{size}each buttonSets height, padding, font size, radius — keep identical across the group
aria-pressed="true" / is-selectedone buttonMarks the selected segment in a single-select group