Segmented control
The segmented track over a value: two to five options in one row, the chosen one on a lifted thumb. A radio group that looks like the Tabs.
Try it
Tab into it and use the arrows; a disabled option is skipped, as with any radio.
Not drawn on its own: the product’s Tabs set is the drawing, and the package has it as the Tabs’ segmented variant. What was missing is the same drawing over a value, which the Tabs page has said since it was written: a control that holds a value and shows no panel is a radio group, however much it looks like the segmented track. This is that radio group. The two share one stylesheet, so they cannot drift apart.
SegmentedControl or Tabs
The question is what the reader is choosing. Tabs switch between views of one object, each a panel complete on its own: the details, the participants and the chat of an appointment. SegmentedControl holds an answer: the period of a calendar, the view of a list, a unit. A screen reader tells them apart, “tab, 2 of 3, selected” against “radio button, 2 of 3, checked”, and so does a form: the control submits its value, the Tabs submit nothing. When the answer changes what is shown below, it is still a SegmentedControl, since what is shown is the same thing filtered, not a different panel.
States
A radio group may have no answer yet, and a value that names nothing, or a disabled option, checks nothing and draws no thumb. Not the Tabs’ fallback to the first: a form must not answer for the reader. disabled on the control disables the group through its fieldset; the value it holds stays in view.
In a form
The radios are real, so the control needs no wiring to submit: give it a name and the value arrives with the form.
Measures
| Part | Value |
|---|---|
| Height | 32, segment 28 |
| Columns | equal; the track fits its labels, or its container with fullWidth |
| Gap between segments | 2 |
| Radius | track 8, thumb 6 |
| Label, Semibold | 12 / 16 |
| Track | surface/sunken, a border/subtle hairline |
| Thumb | surface/overlay on elevation/sm; a border/strong hairline in dark |
The thumb is one element that slides. The columns are equal, so a step is the thumb’s own width plus the gap, and nothing is measured in script; under reduced motion it jumps. The track is surface/sunken with a hairline where the drawing has surface/base with none, the Tabs’ decision of 2026-09-18: base is the canvas, where the drawn track vanishes.
The boundary
The roadmap had this control take the Checkbox’s rule for every option’s unmarked boundary, border/strong at 3:1. It does not, and this is the decision: on an unchecked checkbox the border is the whole control, and here the options are visible text, the chosen one has a shape, and a colour, and the radio announces the state. The track keeps the Tabs’ hairline. The thumb itself is the one exception, in dark only: its surface step above a card (fidelity audit, 2026-09-24) is the thinnest in the system, and it now takes a border/strong hairline of its own there, the same “separate with a border” move the track’s edge already makes against the canvas.
Accessibility
A fieldset with role="radiogroup", named by a legend that is read and not seen. Each segment is a label around a radio kept off screen, so the platform supplies the arrows, Space, the skipping of a disabled option, the submitted value and what is announced; the component writes no keyboard handling. Focus lands on the radio and the segment draws the ring for it.
Using it
import { SegmentedControl } from 'alpenglow';
const [period, setPeriod] = useState('week');
<SegmentedControl
label="Period"
options={[
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
]}
value={period}
onChange={setPeriod}
/>
// In a form: the value is submitted under name, as any radio group's.
<SegmentedControl label="Period" name="period" options={…} defaultValue="week" />| Prop | Type | Default |
|---|---|---|
options | readonly Segmented | required |
label | string | required |
value | string | — |
defaultValue | string | — |
onChange | (value: string) => void | — |
name | string | a generated id |
disabled | boolean | false |
fullWidth | boolean | false |
className | string | — |