Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

SegmentedControl

A compact mutually-exclusive option switch — radiogroup semantics with roving tabindex — for view modes and filter scopes inside a surface.

Import

import { SegmentedControl } from "@roger-emerson/ploxum-primitives";

Usage

const [scope, setScope] = useState<"all" | "p1" | "p2">("all");
 
<SegmentedControl
  ariaLabel="Alert severity scope"
  onValueChange={setScope}
  options={[
    { value: "all", label: "All" },
    { value: "p1", label: "P1" },
    { value: "p2", label: "P2" },
  ]}
  size="sm"
  value={scope}
/>

Props

PropTypeDefaultDescription
optionsSegmentedControlOption<V>[]Segment definitions, in display order. Required.
valueV extends stringThe active option's value. Controlled — required.
onValueChange(value: V) => voidFires on click and on Arrow / Home / End keyboard navigation. Required.
ariaLabelstringARIA label for the radiogroup. Required.
size"sm" | "md""md"sm = 28px segments, md = 36px.
classNamestringMerged onto the root.
refRef<HTMLDivElement>Ref to the root.

Also accepts all HTMLAttributes<HTMLDivElement>.

interface SegmentedControlOption<V extends string = string> {
  value: V;
  label: string;
  icon?: ReactNode; // leading, aria-hidden
}

The generic V flows from options through value and onValueChange, so string-union state types check end-to-end.

Design rules

  • It's a radiogroup with roving tabindex — ArrowLeft/ArrowRight wrap, Home/End jump, and only the active segment is tabbable. Don't wrap segments in extra focusable elements.
  • The active state reads by border (amber-tint) + elevated surface, not hue alone.
  • Use it for 2–5 mutually exclusive view options within a surface. When segments swap entire page regions, use TabBar instead.
  • It's controlled-only — there is no defaultValue. Keep the state in the owning view (or the demo's zustand store).

Storybook

Open SegmentedControl in Storybook ↗