Navigation

Segmented control

One choice out of a few, in a recessed track with a thumb resting in it. Two or five options, all visible at once — past that it is a select.

Usage

import { SegmentedControl } from '@carabine/ui/segmented-control';
<SegmentedControl
  items={[
    { id: 'day', label: 'Day' },
    { id: 'week', label: 'Week' },
    { id: 'month', label: 'Month' },
  ]}
  value={range}
  onValueChange={setRange}
  label="Range"
/>

The track is a hole and the thumb is an object resting in it. That is the whole idea: opaque recess, inset shadows rather than a border, and a thumb that carries a hairline because it is on top of something.

Clipped, not timed

Each label is drawn twice — once in the resting colour, once in the selected one — and the selected copy is clipped to the thumb. So a word the thumb is halfway across is half of each colour, exactly, at every frame.

The obvious alternative is to fade the label at the moment the thumb passes it, and it is wrong in a way you feel before you can name: the crossfade has a duration of its own, so it lags the thumb going one way and leads it coming back. Clipping has no timing to get wrong, because there is nothing to time — there is only where the thumb is.

Examples

Icons

<SegmentedControl
  items={[
    { id: 'grid', label: 'Grid', icon: <LayoutGrid /> },
    { id: 'list', label: 'List', icon: <List /> },
    { id: 'calendar', label: 'Calendar', icon: <Calendar /> },
  ]}
  defaultValue="list"
  label="View"
/>

Full width

<SegmentedControl
  items={[
    { id: 'monthly', label: 'Monthly' },
    { id: 'yearly', label: 'Yearly' },
  ]}
  defaultValue="yearly"
  full
  size={40}
  label="Billing"
/>

Customization

<SegmentedControl
  items={items}
  size={40}        // track height; everything follows it
  radius={12}      // the thumb takes this minus the padding
  padding={4}      // between the track's edge and the thumb
  full
  stiffness={460}  // the thumb's spring
  damping={40}
  theme="light"
/>

Styling reference

The recess is opaque — #0e0e11 dark, #f3f3f4 light — and it is the same material as the switch's track and the text field's well. Three components that are holes, painted the same, so a form does not read as three different depths.

API reference

PropType
classNamestringAdded to the component’s own classes, so yours wins
styleCSSPropertiesMerged after the component’s own inline styles, so yours wins

Colours are data and dimensions are props, and neither covers a margin, a font, or a class from your own system. That is what these are for. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.

PropTypeDefault
itemsSegmentItem[][]
valuestringControlled
defaultValuestringfirst enabled
onValueChange(value: string) => void
sizenumber34Track height; everything follows it
radiusnumbersize × 0.3The thumb takes this minus the padding
paddingnumber3Between the track’s edge and the thumb
fullbooleanfalseSpreads the options across the width
stiffnessnumber460The thumb’s spring
dampingnumber40
disabledbooleanfalse
labelstring'Options'Accessible name of the group
theme'dark' | 'light''dark'

SegmentItem carries id, label, icon and disabled.

Keyboard

Key
TabOne stop for the whole control — it is one control
← →Walks the options, wrapping, skipping disabled ones
Home / EndFirst and last