Segmented Control
A single-select pill control for rescoping a view: chart granularity, permission type, grid/list mode. It shares the track and raised active chip with Tabs variant="secondary", but it is a radiogroup, not a tablist, because nothing behind it is a panel.
Examples
Basic
Scope: 30d
Sizes
Animated chip
Icon only
Overview
import { SegmentedControl } from "@rogo-technologies/ui/segmented-control";
const RANGES = [
{ value: "7d", label: "7 days" },
{ value: "30d", label: "30 days" },
] as const;
<SegmentedControl ariaLabel="Date range" options={RANGES} value={range} onChange={setRange} />;It wraps Base UI Radio, so roving focus, arrow-key selection, and aria-checked come from Base UI rather than hand-rolled key handlers. options is generic over your value union, so onChange hands back your own type, with no cast at the call site.
Usage
Segmented control or tabs?
Use Tabs variant="secondary" when the segments swap panels: it has the same pill treatment, plus tab semantics and an associated panel. Use SegmentedControl when the segments rescope one view that stays put: a chart's granularity, a table's filter, a page's view mode.
Animated chip
animateLayoutId slides one shared chip between segments instead of cross-fading each segment's background. The id only has to be unique among controls mounted at the same time; when a page renders responsive copies of the same control, derive it from useId().
<SegmentedControl animateLayoutId={useId()} ariaLabel="View mode" … />Icon-only segments
iconOnly renders square segments with the icon alone and promotes each option's label to its accessible name, so the control stays screen-reader complete without visible text.
API
SegmentedControl
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | - | Selected option value. |
onChange | (value: T) => void | - | Fired with the newly selected value. |
options | readonly SegmentedControlOption[] | - | Segments, in render order. |
ariaLabel | string | - | Names the group; required because there is no visible group label. |
size | "sm" | "md" | "md" | sm fits inside a chart toolbar. |
iconOnly | boolean | false | Renders each option's icon alone, with its label as accessible name. |
disabled | boolean | false | Disables every segment. |
animateLayoutId | string | - | Slides one shared chip between segments. |
SegmentedControlOption
| Prop | Type | Description |
|---|---|---|
value | T | Option value. |
label | string | Visible label, or accessible name if iconOnly. |
icon | React.ReactNode | Rendered before the label. |
disabled | boolean | Disables this segment only. |
testId | string | Sets data-testid on the segment. |
Guidelines
Do
- Reach for it when the segments rescope a view that stays in place.
- Keep labels to two or three short segments; more belongs in a
Select. - Give every control an
ariaLabelthat names what is being scoped.
Don't
- Don't use it to swap panels. That's
Tabs variant="secondary". - Don't hand-roll the track and chip out of buttons; that's what this replaces.
- Don't reuse one
animateLayoutIdacross two simultaneously mounted controls.