Skip to content

Segmented control

In review
Live · web

View mode: the outline slides; the preview below swaps (Tab in, Arrow keys)

Text only: 2 · 3 · 5 segments

NockerlIcon + label · icon only, sm and md

A disabled segment: skipped by arrow keys, still legible

Full-width: equal-share segments fill the row

Disabled control: dimmed but readable, not interactive

Viewing list · range week · diff unified. The outline slides to the active segment. The island is live.

// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform;
// this is the canonical API the published @dizyx/nockerl-react package exposes.)
import { NockerlSegmentedControl } from '@dizyx/nockerl-react';
export function TaskViewSwitch() {
const [view, setView] = useState('list');
return (
<>
<NockerlSegmentedControl
label="Task view"
value={view}
onChange={setView}
segments={[
{ value: 'list', label: 'List', icon: 'list' },
{ value: 'grid', label: 'Grid', icon: 'grid' },
{ value: 'board', label: 'Board', icon: 'board' },
]}
/>
{view === 'list' ? <TaskList /> : view === 'grid' ? <TaskGrid /> : <TaskBoard />}
</>
);
}
// NockerlIcon-only, compact: pass `icon` + `title` (the accessible name) and no `label`.
<NockerlSegmentedControl label="Appearance" size="sm" value={theme} onChange={setTheme}
segments={[
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
{ value: 'auto', icon: 'auto', title: 'Auto' },
]} />
PropTypeDefaultDescription
segments *Segment[]The segments, left-to-right. 2-5 mutually-exclusive peers.
value *stringThe currently-active value (controlled). Exactly one is selected.
onChange(next: string) => voidFired with the next value. Ignored for a disabled segment.
label *stringAccessible name for the group (wired via aria-label).
sizeSegmentSize'md'Control height + padding + type role.
orientation'horizontal' | 'vertical''horizontal'Track axis. horizontal (default) = segments in a row, pill slides X. vertical = segments stacked, pill slides Y (e.g. an AM/PM meridiem in a narrow slot). Additive: omitting it is a no-op for existing (horizontal) consumers.
fullWidthbooleanfalseStretch to fill the row (equal-width segments) vs. hug its content.
disabledbooleanfalseDim + disable the whole control.

Also accepts the native <div> attributes (e.g. onClick, disabled, id, aria-*, data-*), forwarded straight through, plus a forwarded ref.

Cross-platform drift (tracked for reconciliation)

Segmented control is the connected, single-track view/mode switch, distinct from its neighbours: tabs navigate page/section content (each tab owns a tabpanel; role="tablist"), whereas a segmented control just flips a small set of mutually-exclusive views/modes with no associated panels (role="radiogroup"). A radio group is the same single-choice intent but looser and labelled: separate rows that can carry descriptions; reach for it for 4+ rich choices, and for the segmented control for 2 to 5 compact peers.

Real divergences between the shipped platforms:

  • Active-segment treatment (ratified): the selected segment is the sliding cyan outline, accent border + accent ink with no wash, per design laws §6 and §7, which name the filled cyan pill as the former treatment. The Swift idiom on this page already draws that outline at the selection weight (border.width.selection @ border.opacity.selection); the published Compose control still fills its active segment, so Android is the platform that converges here.
  • The slide. Settled alongside it: the marker animates transform + width, never the fill, wherever the control is ours. Compose already slides its indicator; the Swift idiom cuts straight to the new stroke and adopts the slide.
  • Track surface (ratified): the track is a flat neutral hairline container with zero fill, on the 12px control radius with a 2px inset, grouping the segments as connected peers. It carries no recessed inset-shade, because with the active segment reading by outline there is no fill for a well to sit under. Swift and web both draw this. Compose still uses a cardAlt2 well, which converges when Android does.