Segmented controls
A segmented control is a linear set of two or more segments, each of which functions as a button. One segment is always selected. Use it to switch between views or filter a collection, not to trigger actions.
When to use it
Use a segmented control when people pick one of two to five views that swap in place. Day, Week, Month. List, Grid.
Use tabs when the sections carry their own content areas. Use radios when the choice is part of a form and takes effect on submit. Avoid more than five segments on desktop; labels truncate and targets shrink.
Anatomy
| Part | Value |
|---|---|
| Track | 24px tall, 6px radius, --p-segmented-track background (black 10%, white 9% in dark mode) |
| Segments | Equal width, 54px minimum including padding, 10px horizontal padding |
| Label | 13px, weight 510. Unselected --p-text-primary, selected white |
| Selected pill | Full height, no inset, 6px radius, flat --p-control-accent fill |
| Separators | 1px, black at 25% (white at 25% in dark mode), 14px tall, hidden next to the selection |
Basic use
<div class="p-segmented" role="group" aria-label="Calendar view">
<label class="p-segment"><input type="radio" name="view" checked /><span>Day</span></label>
<label class="p-segment"><input type="radio" name="view" /><span>Week</span></label>
<label class="p-segment"><input type="radio" name="view" /><span>Month</span></label>
</div>
It is a radio group wearing a control. Native inputs carry the state, so keyboard navigation, form submission, and screen reader semantics come free. No JavaScript.
Separators sit between segments and disappear next to the selection, exactly like macOS. Pressing an unselected segment tints it with --p-fill-tertiary before the selection moves.
Sizes
Five sizes share the macOS control ladder with buttons and pop-up buttons. The two largest turn into capsules: the track and the selected pill both round fully.
<div class="p-segmented p-segmented-large" role="group" aria-label="View">
<label class="p-segment"><input type="radio" name="view" checked /><span>Day</span></label>
<label class="p-segment"><input type="radio" name="view" /><span>Week</span></label>
</div>
| Size | Class | Height | Radius | Label | Padding | Separator |
|---|---|---|---|---|---|---|
| Mini | .p-segmented-mini | 16px | 4px | 10px | 6px | 10px |
| Small | .p-segmented-small | 20px | 5px | 11px | 8px | 12px |
| Regular | (default) | 24px | 6px | 13px | 10px | 14px |
| Large | .p-segmented-large | 28px | capsule | 13px | 12px | 18px |
| Extra large | .p-segmented-xlarge | 36px | capsule | 13px | 14px | 20px |
Every label is weight 510. Mini and small are for dense inspectors and toolbars; they sit well below the 44px touch minimum, so keep them to pointer interfaces.
Gray style
<div class="p-segmented p-segmented-gray" role="group" aria-label="Density">
<label class="p-segment"><input type="radio" name="sc-density" checked /><span>Compact</span></label>
<label class="p-segment"><input type="radio" name="sc-density" /><span>Cozy</span></label>
<label class="p-segment"><input type="radio" name="sc-density" /><span>Roomy</span></label>
</div>
.p-segmented-gray swaps the accent pill for a quiet gray pill (black 10% over the track, 19% pressed) and keeps the label color. Consider it for secondary controls that should not compete with the view’s primary action.
Changing the color
<div class="p-segmented p-sys-purple" style="background: var(--p-segmented-track);" role="group" aria-label="Purple example">
<label class="p-segment"><input type="radio" name="sc-tint1" checked /><span>One</span></label>
<label class="p-segment"><input type="radio" name="sc-tint1" /><span>Two</span></label>
</div>
<div class="p-segmented p-sys-green" style="background: var(--p-segmented-track);" role="group" aria-label="Green example">
<label class="p-segment"><input type="radio" name="sc-tint2" checked /><span>One</span></label>
<label class="p-segment"><input type="radio" name="sc-tint2" /><span>Two</span></label>
</div>
Add a .p-sys-* class to retint the selected pill through --p-color-current, the same mechanism as buttons and form controls. The color class paints its swatch background on the track, so restate the track fill with an inline style or a utility class when you tint.
States
- Pressed on the selected pill darkens it by 15% (in dark mode it lightens slightly). Pressed on an unselected segment shows a quiet fill.
- Focus shows the macOS focus band on the focused segment: 3.5px outside, 1px inside, accent at 50%. Arrow keys move the selection, because it is a radio group.
- Disabled: put
disabledon every input. The control fades to 50%, unselected labels drop to#BFBFBF, the selected label to half-opacity white.
Dark mode
The track flips to white at 9%, the gray pill to white at 10%, separators to white at 25%, and the accent pill picks up the dark accent (#0091FF) from colors.css. No extra setup.
The slide
macOS slides the pill between segments, and so does this control, still without JavaScript. The track watches its own radios through :has(), derives the selected index and the segment count, and slides a single pill to index / count of its width over 0.25s. Segments are equal-width grid columns, so the position math is exact at any control width. Browsers without :has() fall back to a 0.15s crossfade between per-segment pills; the markup is identical either way.
Migration from v1
| v1 | v2 |
|---|---|
.p-segmented-controls + .active | .p-segmented with radio inputs |
.p-segmented-controls-alt | .p-segmented-gray (closest) |
.p-segmented-grey | .p-segmented-gray |
.p-segmented-radius, -outline, -outline-alt | .p-segmented-large or -xlarge for a capsule; no outline equivalent |
The v1 classes stay for backwards compatibility and will be removed in a future major. The v2 control needs no .active class management in JavaScript; the checked radio is the state.