Skip to content
Highlight

V2 componentsUpdated Oct 21, 2026

Segmented Control

A pick-one strip for switching a view or a setting in place: a theme, a layout, a billing period. Use it when there are two to five short options and the choice applies at once. Use Tabs when the options are pages of content, a Select for more than five or for long labels, and a RadioGroup when each option needs a sentence. Built on Base UI RadioGroup, so it is one tab stop and the arrows move and choose.

Source: packages/ui/src/components/segmented-control

tsx
import { SegmentedControl, SegmentedControl.Item } from '@highlight/ui'

Every segment is as wide as the widest, so the slab can be one segment wide and travel its own width per step. The chosen one sits on the page’s ground; the rest are in the secondary ink and come up to the text ink under the pointer.

ListBoardCalendar
tsx
import { SegmentedControl } from '@highlight/ui' export default function SegmentedControlBasic() {  return (    <SegmentedControl aria-label="Layout" defaultValue="list">      <SegmentedControl.Item value="list">List</SegmentedControl.Item>      <SegmentedControl.Item value="board">Board</SegmentedControl.Item>      <SegmentedControl.Item value="calendar">Calendar</SegmentedControl.Item>    </SegmentedControl>  )}

A 14px glyph before the label, or the glyph alone with aria-label, where the segment is the track’s height square.

LightDarkSystem
tsx
import { SegmentedControl } from '@highlight/ui'import { Monitor, Moon, Sun } from 'lucide-react' export default function SegmentedControlIcons() {  return (    <div className="flex flex-col items-start gap-4">      <SegmentedControl aria-label="Theme" defaultValue="system">        <SegmentedControl.Item value="light" icon={<Sun />}>          Light        </SegmentedControl.Item>        <SegmentedControl.Item value="dark" icon={<Moon />}>          Dark        </SegmentedControl.Item>        <SegmentedControl.Item value="system" icon={<Monitor />}>          System        </SegmentedControl.Item>      </SegmentedControl>      <SegmentedControl aria-label="Theme" size="sm" defaultValue="light">        <SegmentedControl.Item value="light" icon={<Sun />} aria-label="Light" />        <SegmentedControl.Item value="dark" icon={<Moon />} aria-label="Dark" />        <SegmentedControl.Item value="system" icon={<Monitor />} aria-label="System" />      </SegmentedControl>    </div>  )}

sm (24) for a dense row or a page header, md (28) otherwise. block fills the row. A disabled segment drops to the disabled ink; a disabled control keeps the slab where it was. With no value, there is no slab: nothing is chosen yet.

MonthlyYearly
AllMineShared
LiveFixture
tsx
import { SegmentedControl } from '@highlight/ui' export default function SegmentedControlStates() {  return (    <div className="flex w-80 flex-col items-start gap-4">      <SegmentedControl aria-label="Billing period" size="sm" defaultValue="monthly">        <SegmentedControl.Item value="monthly">Monthly</SegmentedControl.Item>        <SegmentedControl.Item value="annual">Yearly</SegmentedControl.Item>      </SegmentedControl>      <SegmentedControl aria-label="Filter" block>        <SegmentedControl.Item value="all">All</SegmentedControl.Item>        <SegmentedControl.Item value="mine">Mine</SegmentedControl.Item>        <SegmentedControl.Item value="shared" disabled>          Shared        </SegmentedControl.Item>      </SegmentedControl>      <SegmentedControl aria-label="Data" defaultValue="live" disabled>        <SegmentedControl.Item value="live">Live</SegmentedControl.Item>        <SegmentedControl.Item value="fixture">Fixture</SegmentedControl.Item>      </SegmentedControl>    </div>  )}

The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.

SegmentedControl
Base UI RadioGroup. value, defaultValue, onValueChange, disabled, size and block live here; name it with aria-label or a Field label.

Props added on top of the underlying element or Base UI part.

PropTypeDefaultDescription
size'sm' | 'md''md'Track height; inset and corners follow from it.
blockbooleanfalseFill the row instead of sizing to the segments.
value / defaultValuestring—The chosen segment, controlled or not.
onValueChange(value: string) => void—Fires on click or arrow key.
Item iconReactNode—A glyph before the label, sized to 14.
  • Items must be direct children: the control reads the chosen index off them to place the slab.
  • The slab’s edges move on two registered properties, --segmented-lead and --segmented-trail (base.css), the trailing one a third slower, so it stretches across a long throw and settles at its own width. Off under prefers-reduced-motion.