Actions

Segmented

A segmented control is a single-select switch with a sliding indicator.

Components · Actions

A segmented control is a single-select switch: two to five mutually exclusive options laid out in a rounded track, with a sliding indicator that marks the active choice. Built from .nm-segmented wrapping transparent nm-button items, it's the styled, track-backed sibling of the button group — use it to flip a view, mode, or filter where exactly one option is always on.

Overview

A segmented control answers one question: which of these mutually exclusive options is active right now? Every option sits in a shared track; selecting one slides a filled indicator beneath it and flips its label to the active colour. There is always exactly one selection — the control has no empty state.

It looks like a button group, but the two solve different problems:

  • A segmented control is a single-select switch — a styled track with a sliding indicator. The selection is the point.
  • A button group is a cluster of connected buttons — it can be single-select, but it's just as happy holding independent commands (paginate, zoom, undo). No track, no sliding indicator.

When to use

  • To switch a view or layout (List / Board / Calendar)
  • To pick one mode or scope (All / Mine / Shared, Monthly / Yearly)
  • To filter a list by one mutually exclusive facet (Active / Archived)

When not to use

  • For independent commands or steppers — use a button group.
  • For more than ~5 options, or long labels that won't fit on one line — use a select or tabs.
  • To navigate between pages or sub-sections of content — use tabs or a secondary nav.
  • For multi-select (more than one option on at once) — use checkboxes or toggle chips.

Anatomy

A segmented control is a positioned flex track holding an absolutely-positioned indicator and a row of transparent buttons. The buttons sit above the indicator (z-index:1); the indicator slides under the active one with a 0.2s ease-out transition on top / left / width / height.

What drives each part

PartRoleSource
TrackFill behind all items--nm-segmented-bg
TrackBorder--nm-segmented-border
TrackGap / padding / radiusgap:2px, padding:2/4px, border-radius per size
IndicatorFill of the active slot--nm-segmented-item-active-bg
IndicatorBorder--nm-segmented-item-active-border
IndicatorMotiontransition: top/left/width/height 0.2s ease-out
Item labelColour at rest--nm-segmented-item-fg
Item labelColour when selected--nm-segmented-item-active-fg
ItemFill / borderalways transparent — the indicator does the work

Selection & the indicator

The selected item carries aria-pressed="true" (or .is-selected), which flips its label to --nm-segmented-item-active-fg. The visible "pill" behind it is the separate .nm-segmented__indicator element — and its position is set in JavaScript, not CSS. On selection (and on mount/resize), measure the active button and write the indicator's top / left / width / height; the 0.2s transition animates the slide for you.

Click between options to watch the indicator slide. Exactly one item is selected at all times.

  • Keep one option selected at all times — a segmented control never sits empty.
  • Reposition the indicator on selection and on resize, font load, or container width change, or it will drift out of alignment.
  • Order options by how the user reasons about them (chronological, small→large), not alphabetically.
  • Two to five options. Beyond that, labels crowd and the slide stops feeling crisp.

Sizes

Five sizes, set with nm-segmented--{size} on the track. Each pairs a nm-button--{size} item with matching track radius, padding, and indicator radius. Total control height is the item height plus the track padding on both sides.

xs
h 28px · pad 2 · track r 8 · item r 6
sm
h 36px · pad 2 · track r 10 · item r 8
md
h 44px · pad 2 · track r 12 · item r 10
lg
h 56px · pad 4 · track r 16 · item r 12
xl
h 64px · pad 4 · track r 16 · item r 12

Choosing a size

  • xs / sm — toolbars, table headers, dense filter bars
  • md — default for panels, cards, page-level view switchers
  • lg / xl — settings pages, marketing surfaces, touch-first kiosks

Icon variants

Items take the same icon options as buttons: label-only, leading icon + label, or icon-only. Keep the treatment consistent across the whole control — don't mix an icon-only segment with labelled ones.

Icon + label

Icon only

Reserve for universally understood glyphs. Every segment still needs a label for assistive tech — pass it via aria-label.

Icon-only segments · md · each labelled for screen readers.

States

The track and items respond to interaction without ever changing the selection model.

md · the middle segment is selected; hover and focus any other to see the resting states.

Selected vs. hover

  • Selected is persistent: the indicator sits under the active item and its label uses the active colour. It survives focus moving away.
  • Hover on an unselected item is a soft affordance only — it does not move the indicator. The indicator moves on click, then animates into place.

Focus

Each item is a real <button>, so it shows the standard :focus-visible ring. Because items sit above the indicator (z-index:1), the ring is never clipped. Never suppress it.

No disabled state

Like buttons, segmented controls don't ship a disabled appearance. If an option isn't currently valid, keep the control live and surface the reason inline rather than greying out a segment — a dead segment leaves users guessing.

Light & dark theme

The --nm-segmented-* tokens remap under data-theme. In light theme the indicator is near-black with light text; in dark theme it inverts to a light pill with dark text. Track, indicator, and label colours all flip automatically.

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.

Segmented in context

Where a segmented control fits naturally. These are layout recommendations, not rigid rules.

List header — view switcher

Documents

A quiet segmented switch for the view, a single loud accent button for the primary action.

Filter scope

Pricing toggle

Choose a billing period
Save 20% when you pay annually

Do & don't

The rules that keep a segmented control crisp and unambiguous.

✓Do
Use 2–5 short, parallel options that read as one set. Keep exactly one selected.
✕Don't
Don't pack long labels into segments — they crowd the track and the control stops reading at a glance. Use tabs or a select.
✓Do
Use a segmented control to switch one view or mode where every option is mutually exclusive.
✕Don't
Don't use a segmented control for independent commands like pagination — that's a button group.
✓Do
Always keep one segment selected — the indicator tells users which mode they're in.
✕Don't
Don't leave the control with nothing selected — the indicator vanishes and the current mode is unreadable.
✓Do
Keep the icon treatment uniform — either every segment has a leading icon or none do.
✕Don't
Don't mix icon-only, icon+label, and label-only segments in one control — the row reads as broken.

Accessibility

A segmented control is a single-select widget. Get the role and the selected-state attribute right and the rest follows from the native buttons.

Choosing a role

  • Toggle-button pattern (used here): role="group" with an aria-label on the track, and aria-pressed="true|false" on each <button>. Simple and robust.
  • Radio pattern (stricter): role="radiogroup" on the track, role="radio" + aria-checked on each item, with arrow-key roving focus so the control is a single Tab stop. Prefer this when the segmented control behaves exactly like a radio group.

Pick one and apply it consistently — don't mix aria-pressed with role="radio".

Keyboard

  • In the toggle pattern, Tab moves between segments and Space / Enter selects the focused one.
  • In the radio pattern, Tab enters the group once; arrow keys move and select within it, per the WAI-ARIA radio pattern.

Labeling

  • Give the track an aria-label (or aria-labelledby) naming what it controls ("View", "Billing period").
  • Icon-only segments each need their own aria-label describing the option, not the glyph.
  • Reflect the choice with aria-pressed / aria-checked — never rely on the indicator's colour alone. The indicator is aria-hidden.

Contrast & motion

  • Selected and unselected labels meet WCAG 2.2 AA against their backgrounds in both themes.
  • The 0.2s indicator slide is a short, non-essential transition; respect prefers-reduced-motion by disabling it for users who ask.

Playground

Click a segment to slide the indicator. The markup updates live — copy it straight into your product (and wire up the indicator positioning in JS).

<div class="nm-segmented nm-segmented--md" role="group"> <span class="nm-segmented__indicator"></span> <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

The track and indicator own a dedicated set of --nm-segmented-* custom properties; geometry comes from the size modifier classes.

Colour tokens

TokenRoleLightDark
--nm-segmented-bgTrack fillsurface-100neutral-950
--nm-segmented-borderTrack borderneutral-500neutral-50
--nm-segmented-item-active-bgIndicator fillneutral-950neutral-50
--nm-segmented-item-active-borderIndicator borderneutral-950neutral-50
--nm-segmented-item-fgLabel at restsurface-950surface-50
--nm-segmented-item-active-fgSelected labelsurface-50surface-950

Per-size geometry

Propertyxssmmdlgxl
Item height24px32px40px48px56px
Track padding2px2px2px4px4px
Track radius8px10px12px16px16px
Indicator / item radius6px8px10px12px12px
Track gap2px2px2px2px2px

Values are baked into .nm-segmented--{size} and the child .nm-button--{size} — use a different size class rather than overriding them.

Implementation

Wrap a row of transparent nm-button items in .nm-segmented, add a single .nm-segmented__indicator element, and mark the active item with aria-pressed="true". The CSS handles colours and the slide transition; you position the indicator in JavaScript by measuring the active button.

<link rel="stylesheet" href="namirial-theme.css">

<div class="nm-segmented nm-segmented--md" role="group" aria-label="View">
  <span class="nm-segmented__indicator"></span>
  <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>
// Position the indicator under the active segment, then on every change.
function moveIndicator(root) {
  const active = root.querySelector('[aria-pressed="true"]');
  const bar = root.querySelector('.nm-segmented__indicator');
  if (!active || !bar) return;
  bar.style.left   = active.offsetLeft + 'px';
  bar.style.top    = active.offsetTop + 'px';
  bar.style.width  = active.offsetWidth + 'px';
  bar.style.height = active.offsetHeight + 'px';
}

document.querySelectorAll('.nm-segmented').forEach(root => {
  moveIndicator(root);
  root.addEventListener('click', e => {
    const btn = e.target.closest('.nm-button');
    if (!btn) return;
    root.querySelectorAll('.nm-button').forEach(b => b.setAttribute('aria-pressed', b === btn));
    moveIndicator(root);
  });
  window.addEventListener('resize', () => moveIndicator(root));
});

CSS classes

ClassApplies toEffect
nm-segmentedcontainerTrack: background, border, padding, gap, positioning context
nm-segmented--{size}containerTrack radius & padding — xs · sm · md · lg · xl
nm-segmented__indicatorone childThe sliding pill; position it in JS
nm-button nm-button--secondary nm-button--{size}each itemTransparent button; match the size to the track
aria-pressed="true" / is-selectedone itemMarks the active segment and flips its label colour