Segmented control
StableA radio group drawn as one strip, with a lit pill that glides to the choice and arrow keys that move and choose.
For two to five short, mutually exclusive choices that change a view straight away: a period, a layout, a mode. The pill is the paper button finish; it measures the chosen segment and slides there, and before that measurement (in the prerendered HTML) the chosen segment draws its own background.
Switching a view
import { useState } from 'react'import { Segmented } from '@rchursin/product/ui'
type Period = 'week' | 'month' | 'quarter'
// Sample numbers for the democonst focus: Record<Period, { hours: string; sessions: number }> = { week: { hours: '11 h 40 min', sessions: 23 }, month: { hours: '48 h 15 min', sessions: 96 }, quarter: { hours: '139 h 50 min', sessions: 281 },}
/** * One Tab stop for the whole strip; ← → (or ↑ ↓) move and choose at once, so the numbers * change as you go. The lit pill glides to the choice. */export default function SegmentedPeriod() { const [period, setPeriod] = useState<Period>('week') const { hours, sessions } = focus[period]
Usage
import { Segmented } from '@rchursin/product/ui'<Segmented label="Period" value={period} onChange={setPeriod} options={[ { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }, ]}/>Examples
Sizes and icons
md (32px) with icons beside the words, and sm (28px) for toolbars.
import { useState } from 'react'import { Segmented } from '@rchursin/product/ui'
type Mode = 'write' | 'preview'type View = 'list' | 'board'
/** md (32px) with icons beside the words, and sm (28px) for toolbars. Icons never replace the words. */export default function SegmentedSizes() { const [mode, setMode] = useState<Mode>('write') const [view, setView] = useState<View>('list')
return ( <div className="flex flex-col items-start gap-6"> <Segmented<Mode> label="Note editor mode" value={mode} onChange={setMode} options={[ { value: 'write', label: 'Write', icon: 'pencil' }, { value: 'preview', label: 'Preview', icon: 'eye' }, ]} /> <Segmented<View> label="Show tasks as" size="sm" value={view} onChange={setView} options={[ { value: 'list', label: 'List', icon: 'list' }, { value: 'board', label: 'Board', icon: 'board' }, ]} /> </div> )}API
Segmented control props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | – | The group’s accessible name (aria-label). It is not drawn, so make the choice clear from the context or a nearby heading. |
optionsrequired | { value: V; label: ReactNode; icon?: IconName }[] | – | The segments, in order. An icon sits before its label. |
valuerequired | V | – | The chosen value; the chosen segment is the only one in the tab order. Keep it one of the options: if it matches none, nothing is checked and the first segment takes the Tab stop. |
onChangerequired | (v: V) => void | – | Called on click and on every arrow key press. |
size | 'sm' | 'md' | 'md' | Height 28 or 32 pixels. |
className | string | – | Classes on the strip. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves into the strip, onto the chosen segment, and out again. One Tab stop. |
| Right arrowDown arrow | Chooses the next segment and moves focus to it; wraps from the last to the first. |
| Left arrowUp arrow | Chooses the previous segment; wraps from the first to the last. |
- role="radiogroup" with an aria-label, and role="radio" with aria-checked on each segment, so screen readers say “Month, radio button, 2 of 3”.
- Only the chosen segment has tabIndex 0 (a roving tab index); the arrow keys move focus and choose together, like native radios.
- The arrow keys are caught on the strip, so the page does not scroll while you use them.
- The pill is aria-hidden decoration. The chosen segment also changes text from fg-3 to fg, so the state never rests on the pill alone. The glide only runs when motion is allowed.
Guidelines
Do
- Use it for 2 to 5 short labels that switch a view in place.
- Keep labels to one or two words; add an icon only beside a word.
- Put it close to what it changes.
Don’t
- Don’t use it for navigation between pages or panels with their own content: use Tabs.
- Don’t use it in a form that waits for Submit: use RadioGroup.
- Don’t squeeze in more options than fit on a phone without scrolling.
In use
Used on this site
Related
- TabsSwitch between views of the same thing. A lit pill or a hairline glides to the selected tab; arrow keys move between tabs.
- Radio groupA fieldset of native radios, each a ring with the brand square inside when chosen, with optional hints.
- SwitchAn on/off switch for settings that apply at once: label and hint on the left, a sunken track with a pearl thumb on the right.
Source
The component’s own files, exactly as they run on this site.
/** * Form fields. Labels sit above inputs, hints under the label, errors between the hint and the * input with an icon and the word "Error:" for screen readers (never colour alone). * Optional fields say "(optional)"; required ones carry no mark. * * <Field label="Email" hint="We send the sign-in code here." error={err}> * {(p) => <TextInput {...p} type="email" autoComplete="email" />} * </Field> */import { forwardRef, useId, useLayoutEffect, useRef, useState, type InputHTMLAttributes, type KeyboardEvent, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes,