videorc/ui

Segmented Control

Pick one of 2–4 exclusive options on the glass Tabs track. A radio group with roving focus and one tab stop, manual activation for heavy changes, and a disabled state that always says why.

Installation

$ pnpm dlx shadcn@latest add @videorc/segmented-control

Usage

import { SegmentedControl } from "@/components/ui/segmented-control";
<SegmentedControl
  aria-label="Theme"
  onValueChange={setTheme}
  options={[
    { label: "Light", value: "light" },
    { label: "Dark", value: "dark" },
    { label: "System", value: "system" },
  ]}
  value={theme}
/>

Which control when:

  • Segmented control chooses a value: theme, fit, corner, kind of cut.
  • Tabs switch a view, a page or a pane with its own content.
  • Toggle group is for filter chips: several on at once, or a filter row.
  • A Switch is for a boolean, and a one-shot action is a Button.

Examples

Sizes

size="sm" (20 px segments) fits a toolbar or a small window. activationMode="manual" makes the arrow keys only move focus, and Space or Enter choose: use it when a change is heavy, such as switching a live scene.

Disabled with a reason

A disabled control always says why. Pass disabledReason with disabled and the reason shows as a line under the control and describes the group.

Locked while you’re recording or live. Stop to switch orientation.

Icons

Draw an option as an icon with an aria-label, and give it a tooltip: its name on hover and on focus, never a native title.

Theming

Segmented control wears the same track and segment as Tabs. Both read it from tabs-variants, a small module with no Base UI import, so a segmented control costs a window's first bundle nothing:

  • tabsListVariants({ variant }) is the track.
  • tabsTriggerBaseClassName is one segment, before its selected state.
Token or attributeMeaning
bg-foreground/5, ring-borderThe recessed track
chrome-plateThe checked segment, keyed off aria-checked
rounded-row, rounded-chipThe track and a segment
min-h-control, min-h-6The default and sm track heights
data-checked, data-uncheckedOn every segment, as a Base UI radio marks it
data-sizeOn the track: default or sm

Keyboard

KeyAction
TabLands on the checked option, or the first enabled one; Tab again leaves the group
ArrowRight, ArrowDownMoves to the next enabled option, wrapping; checks it unless activation is manual
ArrowLeft, ArrowUpMoves to the previous enabled option, wrapping; checks it unless activation is manual
Home, EndMoves to the first or last enabled option
Space, EnterChecks the focused option

Accessibility

It follows the WAI-ARIA radio group pattern: the track is a radiogroup and each option a radio with aria-checked. Name it with aria-label, or put it in a Field: the Field's label names the group through aria-labelledby unless the group carries its own aria-label. While disabled with a reason, the group is aria-disabled and aria-describedby the reason, so a screen reader hears why.

onValueChange fires only with a newly checked value. An option's own onClick still runs on the checked option: the hook for choosing it again (replay, re-apply).

API reference

Every div prop, plus:

Prop

Type

SegmentedControlOption

Prop

Type

segmentedTargetIndex(enabled, current, key) and segmentedTabStop(enabled, checkedIndex) are the keyboard rules as pure functions, for a control that needs the same roving focus.