v0.5

Segmented Control

Single-select switcher for 2 to 5 inline options, with a flat row or grouped pill style.

Description

SegmentedControl is a compound component (SegmentedControl + SegmentedControlItem) that renders a <div role="group"> of real <button> items. It has two visual modes: flat is a row of independent buttons, grouped is the classic segmented pill with a sliding indicator that animates between the active option.

Reach for it when you have 2 to 5 mutually exclusive options that benefit from being visible all at once. Typical surfaces: view switchers (grid, list, chart), time-range pickers (day, week, month), layout toggles, and inline tabs that don't change the route.

If the list grows past about 5 options, use Select. If each option swaps a route or a content panel, use Tabs. If the choice is a boolean, use Switch. If users can pick more than one, use ToggleGroup or CheckboxGroup. If you need the standard single-tab-stop arrow-key radio model, use RadioGroup.

Installation

pnpm dlx @create-ui/cli add segmented-control

Anatomy

SegmentedControl provides a context that hands variant, shape, size, and appearance down to its items, plus an optional value binding. Each SegmentedControlItem is a self-contained <button> (or any element via asChild).

<SegmentedControl value="..." onValueChange={...}>
  <SegmentedControlItem value="..." />
  <SegmentedControlItem value="..." />
  <SegmentedControlItem value="..." />
</SegmentedControl>

Usage

import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/ui/segmented-control"
<SegmentedControl defaultValue="week">
  <SegmentedControlItem value="day">Day</SegmentedControlItem>
  <SegmentedControlItem value="week">Week</SegmentedControlItem>
  <SegmentedControlItem value="month">Month</SegmentedControlItem>
</SegmentedControl>

Examples

Variants

variant="primary" paints the active option with the primary token; variant="neutral" keeps it grayscale. Pick neutral when the segmented control sits next to other primary actions you don't want to compete with.

Appearance

appearance="flat" is a row of independent buttons styled with bg-weak at rest; activate them yourself with the selected prop on the chosen item. appearance="grouped" shares a single rounded container and animates a sliding indicator between the active item.

Sizes

Five sizes: xs, sm, md (default), lg, xl. Heights, paddings, type, and item radii all scale together.

Shape

shape="rounded" (default) follows the size's radius scale; shape="pill" makes both the container and the items fully rounded.

With Icon

Pass leadingIcon or trailingIcon to any item. Icon sizing is driven by the active size, so you don't size the SVG yourself.

Icon Only

Set iconOnly on each item for a square footprint with no horizontal padding. Always pair with aria-label so the button announces its purpose.

Controlled

Pass value and onValueChange to drive selection from your own state. Use uncontrolled defaultValue for simple toggles; reach for controlled when other UI needs to react to the change.

View: list

Accessibility

The root is a role="group". Each item is a real <button> with aria-pressed reflecting the active option and aria-disabled when disabled. Every item is its own tab stop (there is no roving tabindex); if you need single-tab-stop arrow-key navigation, use RadioGroup instead.

KeyDescription
TabMoves focus across items; each item is a tab stop.
Space EnterActivates the focused item.

ARIA notes:

  • The root sets role="group". Pair it with aria-label or aria-labelledby when the surrounding context doesn't already describe the choice.
  • Items set aria-pressed to true or false so screen readers announce the toggle state.
  • When iconOnly is set, the item has no visible text, so aria-label is required on each item.
  • Disabled items get aria-disabled and skip click handlers.

Styling

Tailwind override: pass className to merge Tailwind classes with the component's CVA classes (via cn()):

<SegmentedControl className="w-full">
  <SegmentedControlItem value="day" className="flex-1">
    Day
  </SegmentedControlItem>
</SegmentedControl>

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="segmented-control" on the root element.
  • data-slot="segmented-control-indicator" on the sliding pill (only rendered when appearance="grouped").
  • data-slot="segmented-control-item" on each item.
  • data-slot="segmented-control-item-icon" on each leading and trailing icon wrapper.
  • data-slot="segmented-control-item-content" on the inner text wrapper.
  • data-variant="<variant>", data-shape="<shape>", data-size="<size>", data-appearance="<appearance>" on both root and item.
  • data-state="on" | "off" on each item; data-value="<value>" on each item.

Target the active item in CSS:

[data-slot="segmented-control-item"][data-state="on"] {
  /* ... */
}
  • RadioGroup: use this for the standard single-tab-stop arrow-key radio model with a hidden native input.
  • Tabs: use this when each option swaps a content panel or routes to a different view.
  • ToggleGroup: use this when users can select more than one option at a time.
  • Switch: use this when the choice is a boolean.
  • Select: use this when the list grows past about 5 options.

API Reference

SegmentedControl

Root that owns the selection state and provides the variant context. Extends React.ComponentProps<"div">, so standard div attributes (id, aria-label, aria-labelledby, etc.) are accepted.

Props

PropTypeDefaultDescription
valuestring-Controlled selected value.
defaultValuestring-Uncontrolled initial value.
onValueChange(value: string) => void-Fires when the user activates an item with a value.
classNamestring-Tailwind classes merged with the component's CVA classes via cn().
childrenReact.ReactNode-One or more SegmentedControlItem children.

Variants

VariantOptionsDefaultDescription
variant"primary" "neutral""primary"Semantic color of the active option.
shape"rounded" "pill""rounded"pill is fully rounded; rounded follows the size scale.
size"xs" "sm" "md" "lg" "xl""md"Size scale; controls height, padding, type, icon size, and item radius.
appearance"flat" "grouped""flat"flat is a row of independent buttons; grouped renders a sliding pill.

SegmentedControlItem

One option in the control. Extends Omit<React.ComponentProps<"button">, "value">. Renders a <button> by default, or any element via asChild (Radix Slot).

Props

PropTypeDefaultDescription
valuestring-Value committed to the parent's onValueChange when the item is activated.
selectedboolean-Forces the active state. Useful for appearance="flat" when you don't bind a value on the root.
disabledboolean-Disables the button; click handlers are skipped and aria-disabled is set.
leadingIconReact.ReactNode-Icon rendered before the label. Hidden when iconOnly is true.
trailingIconReact.ReactNode-Icon rendered after the label. Hidden when iconOnly is true.
asChildbooleanfalseRender as the child element via Radix Slot; useful for links and custom triggers.
classNamestring-Tailwind classes merged with the component's CVA classes via cn().
childrenReact.ReactNode-Item content. Hidden when iconOnly is true.

Variants

VariantOptionsDefaultDescription
variant"primary" "neutral"inherits rootOverride the parent's variant on a single item. Rarely needed.
shape"rounded" "pill"inherits rootOverride the parent's shape on a single item. Rarely needed.
size"xs" "sm" "md" "lg" "xl"inherits rootOverride the parent's size on a single item. Rarely needed.
appearance"flat" "grouped"inherits rootOverride the parent's appearance on a single item. Rarely needed.
iconOnlybooleanfalseSquare footprint with no horizontal padding; requires aria-label.