Segmented Control
Choose one option from a small set, with the selection always visible.
import { SegmentedControl, SegmentedControlItem } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a segmented control for a small, fixed set of options where one is always chosen and the choice changes a view in place: a time range, a billing period, a layout. It's a thin wrapper over Toggle Group's segmented look (plain labels on a hairline, a 2px accent bar under the pressed one) with a simpler contract: the value is one string, and it can't be emptied.
Reach for something else when the shape is different:
- Toggle Group when several options can be on at once (bold, italic, underline), or when the choice may be cleared.
- Tabs when each option owns a panel of content that the others hide.
- Radio Group for a choice inside a form that's submitted later.
- Select when there are more options than fit in a row.
import { SegmentedControl, SegmentedControlItem } from '@youj/react'
export function RangePicker() {
const [range, setRange] = useState('7d')
return (
<SegmentedControl aria-label="Range" value={range} onValueChange={setRange}>
<SegmentedControlItem value="24h">24h</SegmentedControlItem>
<SegmentedControlItem value="7d">7d</SegmentedControlItem>
<SegmentedControlItem value="30d">30d</SegmentedControlItem>
</SegmentedControl>
)
}onValueChange receives the pressed segment's value as a string. Pressing the segment that's already pressed does nothing, so there's always a choice. variant="pill" sets the segments in a recessed track with a raised key instead, which suits a control that sits inside a capsule or on a well. Leave defaultValue out to start with nothing pressed; the first press picks one.
Description
A range control that works as a card's header and swaps the visitor total as you pick 24h, 7d, 30d or 90d.
The three sizes beside each other.
size is sm, default or lg. Each is the height of the matching control, so a segmented control sits in a row with inputs and buttons of the same size. Icon-only segments need an aria-label. Give the control className="w-full" to stretch it, and the segments share the width.
Code
segmented-control-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsvalue, defaultValue, onValueChange… (8)
Props
SegmentedControl
| Prop | Type | |
|---|---|---|
variant | 'underline' | 'pill'
default 'underline' | 'underline' |
value | stringThe pressed segment's value (controlled). | – |
defaultValue | stringThe segment pressed at first (uncontrolled). Leave it out to start with none pressed. default '' | '' |
onValueChange | (value: string) => voidCalled with the newly pressed segment's value. Never called with an empty value. | – |
aria-label | stringNames the control for assistive tech, e.g. "View" or "Range". | – |
sound | booleanWith sound on ( default true | true |
size | 'default' | 'sm' | 'lg' | – |
Other props pass through: extends ToggleGroupProps. | ||
It also takes Toggle Group's size, orientation, loopFocus and disabled, and the props of a div.
SegmentedControlItem
| Prop | Type | |
|---|---|---|
size | 'default' | 'sm' | 'lg' | – |
valueRequired | stringThe value this segment stands for. | – |
Each segment takes a value and disabled, and the props of a button.
MotionIndicator, indicator appears, press… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Indicatorglides to the pressed segment and retargets if you press again mid-glide | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Indicator appearswhen a control with no value gets its first | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Presseach segment scales to 0.97 while held | duration.instantease.standard80ms · cubic-bezier(0.2, 0, 0, 1) | 80mscubic-bezier(0.2, 0, 0, 1) | |
| Hover and text color | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
The indicator jumps to the pressed segment, and segments don't scale when pressed.
The bar (or the key, in pill) is one Sliding Indicator: transform-only layout projection, so its corners stay round in flight and nothing around it reflows. Resizes and font loads move it into place without animating.
Reduced motion
The indicator moves straight to the pressed segment, and presses don't scale. Which segment is pressed reads exactly the same.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The control is a
groupnamed by itsaria-label, and each segment is a<button>witharia-pressed. - It's a single tab stop. The arrow keys move between segments and wrap at the ends,
HomeandEndjump to the first and last, andSpaceorEnterpresses the focused one. - The indicator is decorative (
aria-hidden), so roving focus only visits real segments. - Name icon-only segments with
aria-label.