Segmented Control

A small set of mutually exclusive choices shown at once, built from real radio inputs in a fieldset — so arrow keys, form submission and screen-reader announcement all work without any script.

src/components/ui/segmented-control.tsx

About

From the component's own source, where it was written.

A small set of mutually exclusive choices, shown all at once.

**Not a button group, and not a tab list.** A button group is a row of independent actions; tabs switch which panel is visible and carry aria-controls. This picks a *value* — day/week/month, grid/list — so it is a radio group, and it is built as one: real <input type="radio"> elements in a <fieldset>. That gives arrow-key navigation, form submission, :checked, and screen-reader announcement of "2 of 3" for free, none of which a row of <button>s with aria-pressed gets right.

**Use it below about five options, with short labels.** Past that a Select is faster to read and does not fight for horizontal space; this is the control you reach for when seeing all the options at once is the point.

The moving indicator is one absolutely-positioned element sized by the selected index, rather than a background on the selected item — so it slides between options instead of blinking, and there is one thing to animate.

Install

npx astralyx-ui add segmented-control

Usage

import { SegmentedControl } from '@/components/ui/segmented-control'

<SegmentedControl label="View" options={options} value={view} onValueChange={setView} />

Composer

Change the props and the source below follows.

View
Props
tsx
<SegmentedControl
  label="View"
  options={options}
  size="default"
  fullWidth={false}
/>

Examples

Choosing a view

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

SegmentedControl

Extends Omit<ComponentProps<'fieldset'>, 'onChange' | 'defaultValue'>.

PropTypeDefaultDescription
disabledboolean——
namestring——
optionsSegment[]required{ value, label, srLabel?, disabled? }. Keep it under about five with short labels.
valuestring——
defaultValuestring——
onValueChange(value: string) => void——
labelstring—Accessible name for the group. Required unless one is supplied elsewhere.
sizeSegmentedControlSize'default'The Button's scale: the track is the height of the Button of the same size, so a segmented control sits level in a toolbar. md is kept as another name for default.
fullWidthbooleanfalseFill the container, splitting the width evenly.

Types

Segment

A small set of mutually exclusive choices, shown all at once.

FieldTypeDescription
valuestring—
labelReactNode—
srLabel?stringAnnounced when the visible label is an icon.
disabled?boolean—
TypeDefinitionDescription
SegmentedControlSize
SegmentSize | 'md'
—

Behaviour

TopicDetailDescription
radios, not buttonsfieldsetIt picks a value, so it is a radio group. A row of buttons with aria-pressed gets arrow keys, :checked and form submission wrong.
vs ButtonGroup / Tabsdifferent jobsA button group is independent actions; tabs switch a visible panel and carry aria-controls. This sets a value.
indicatorone elementPositioned by index rather than a background on the selected item, so it slides instead of blinking and there is one thing to animate.