SegmentedControl
Preview appears after the next Storybook update.
<SegmentedControl />When to use
Use it for
- Switching how the same content is shown: Light or Dark, Comfortable or Compact, Grid or List.
- Short options where seeing every option at once helps the choice: two to five usually, more for filters people switch between often (status filters). On a phone the track scrolls sideways and never widens the page.
- Appearance strong when the control is what the view is about (the status filter of a list): the selected segment fills with Deep Harbor. At most one strong control per view.
- The change applies immediately, with no Save button.
- Size sm in dense toolbars, menus and panels; md everywhere else, including a page's filter bar, where it matches the md search field and Select beside it. Heights follow the density setting.
- fullWidth inside a narrow panel or menu, so the segments share the width equally.
Not for
- Switching between panels of different content: use Tabs, which link each tab to its panel.
- A setting in a form that is saved later: use RadioGroup, which shows a label and help per option.
- Long labels, or options people pick once and forget: use Select.
- Turning one thing on or off: use Switch.
- Triggering actions (Export, Delete): use Button or an attached ButtonGroup.
Do and don't
Every example is rendered with the real component, so it always matches what ships.
Short, parallel labels
One or two words per segment, same grammatical form, so the set reads as one choice.
Label outside the track
Put a visible label to the left of the track, as a caption on its center line, and name the group with it (aria-labelledby, or the same words in label). The track holds the options and nothing else.
Strong for the control the view is about
A list filtered by status: the selected filter fills with Deep Harbor, the strongest signal on the screen. Six short filters are fine; on a phone they scroll sideways.
Always start with a selection
A segmented control shows the current mode. Pass value or defaultValue so one segment is always selected.
Actions dressed as segments
Segments select a mode; they never run a command. "Export" and "Share" belong on buttons.
The label inside the track
Never put the group's label in the same pill as the options. Inside one container it reads as one more option ("Brand" next to "Bridge" and "Ocean"). Keep it outside, to the left.
Too many or too long options
Two or three words per segment make the track longer than the screen and stop reading as one control. Keep labels to one or two words, or use Select.
Accessibility
- Base UI RadioGroup with native button radios: role radiogroup on the row, role radio and aria-checked on each segment.
- Tab enters the group on the selected segment and leaves it; arrow keys move the selection, Home and End jump to the ends.
- Give the group a name: aria-labelledby pointing at the visible label outside the track, or label (aria-label) when the context already names it.
- The selected segment is marked by a raised plate with a checked-color edge (or the Deep Harbor fill with strong) and a darker text color, not by color alone.
- When the track scrolls, arrow keys still move the selection and the selected segment scrolls into view.
- The focus ring (focus.ring.color) shows on keyboard focus only.
- Rendered without JavaScript, the HTML keeps role, aria-checked and data-value, so a small script can drive it: flip aria-checked and tabindex, and handle arrow keys.
Content guidelines
- Sentence case nouns or adjectives: "Compact", "Dark", "Grid". No verbs: segments are states, not actions.
- One or two words. Drop the shared word: "Light, Dark", not "Light theme, Dark theme".
- Name the group with what is being chosen: "Density", "Brand", "Themes shown". When it changes only a preview, say so: "Preview brand".
- A visible label is a caption to the left of the track (caption size, muted text color), never a segment inside it.
From the code Generated from the component, not editable here.
Anatomy and tokens
Read from packages/react/src/components/SegmentedControl/SegmentedControl.module.css at build time. Every value is a token: open one to see its alias chain in both themes.
- Track holding the segments
.root - Size sm
.sm - One option (native button, role radio)
.segment - Size sm
.sm .segment - One option (native button, role radio), hover
@media (hover: hover) .segment:hover:not(:disabled, [data-disabled], [aria-checked="true"]) - One option (native button, role radio)
.segment:active:not(:disabled, [data-disabled], [aria-checked="true"]) - One option (native button, role radio)
.segment[aria-checked="true"] - One option (native button, role radio), focus
.segment:focus-visible - Strong appearance: Deep Harbor selected segment, focus
.strong .segment[aria-checked="true"]:focus-visible - One option (native button, role radio), disabled
.segment:disabled, .segment[data-disabled] - Strong appearance: Deep Harbor selected segment
.strong .segment[aria-checked="true"] - One option (native button, role radio), disabled
.segment[aria-checked="true"]:disabled, .segment[aria-checked="true"][data-disabled]
Motion and interaction
SegmentedControl animates on hover and press. Try it in the preview at the top.
| When | What changes | Duration | Easing |
|---|---|---|---|
| Hover | Text color | motion.duration.instant75 ms | motion.easing.standardsettles in place |
| Press | Background | motion.duration.instant75 ms | motion.easing.standardsettles in place |
With reduced motion turned on, every change happens at once.
From the code Generated from the component, not editable here.
Props
Parsed from the TypeScript types in packages/react/src/components/SegmentedControl/SegmentedControl.tsx, so this table cannot drift from the code. It also accepts every native <div> attribute except children, defaultValue, onChange, aria-label, dir.
| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | SegmentedControlItem[] | none | The segments, two or more. An array (not children) so the control also renders from Astro and other static templates. When they do not fit (six filters on a phone), the track scrolls sideways inside its container and never widens the page; the selected segment is scrolled into view. |
label | string | none | Accessible name of the group. Required unless you pass `aria-labelledby` pointing at a visible label. |
value | string | none | No description in the code yet. |
defaultValue | string | none | No description in the code yet. |
onValueChange | (value: string) => void | none | No description in the code yet. |
size | 'sm' | 'md' | 'md' | Default `md`. Use `sm` in toolbars and dense panels. Heights follow the density tokens. |
appearance | 'default' | 'strong' | 'default' | Default `default`. `strong` fills the selected segment with Deep Harbor: one per view at most. |
fullWidth | boolean | false | Stretch to the container; segments share the width equally. |
disabled | boolean | none | No description in the code yet. |
name | string | none | Form field name: the selected value is submitted with the form. |
Comes with markSegmentOverflow, each accepting native <div> attributes.
Playground
Change the props and see the real SegmentedControl update. Controls are generated from the TypeScript props; nothing here changes the system.
<SegmentedControl label="Density" defaultValue="comfortable" />Workbench
For contributors: every state with the accessibility panel, in the component workbench (Storybook). The stories double as visual test fixtures; the guidance on this page is the user documentation (open SegmentedControl in Storybook).
Embed Storybook stories here (10)
Loads from https://bridge-storybook.floriangouloubi.com (set STORYBOOK_URL at build time). If Storybook is not running, the frames stay empty.
Default
Small
WithIcons
ThreeOptions
FullWidth
Disabled
Strong
ManyOnPhone
WithVisibleLabel
AllStates
From the code Generated from the component, not editable here.
Changelog
- New
SegmentedControl: pick one of two to five views or modes in a row of attached buttons (Base UI RadioGroup with native button radios)
v1.0.0, Nov 11, 2026
- Fix
Target sizes: SegmentedControl
smsegments are at least the small control height (32px), and standalone Links are at least 24px tall (WCAG 2.5.8)v1.0.0, Nov 11, 2026
- Fix
SegmentedControl docs: a visible label goes outside the track, to its left, and names the group with aria-labelledby
v1.0.0, Nov 11, 2026