<!-- Protocore Design System — SegmentedControl -->
# SegmentedControl

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/segmented-control`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { SegmentedControl } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/segmented-control

> Compact single-select toggle — a bordered row of mono uppercase segments where the active one inverts to a solid fill.

## When to use it

Use a **SegmentedControl** for **2–4 short, mutually-exclusive** options that switch a view or scope in place — a time range, a layout mode, an environment. It's the compact cousin of **RadioGroup**: prefer RadioGroup when options are longer or need descriptions, and **Select** once they outgrow a row. It differs from **FilterBar** in intent — a segmented control picks one *mode*, while a FilterBar toggles filters (often many) over a collection. It is not a **Switch**: use Switch for a single on/off setting.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | — | Accessible name for the radio group (required for icon-only segments). |
| `className` | `string` | no | — | — |
| `defaultValue` | `string` | no | — | Uncontrolled initial selected value. |
| `disabled` | `boolean` | no | — | Disable the whole control. |
| `items` | `SegmentedControlItem[]` | yes | — | The segments, left to right. |
| `onValueChange` | `((value: string) => void)` | no | — | Fired with the newly-selected value (never with an empty string — a segmented control always keeps one segment active). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `string` | no | — | Controlled selected value. |

## Examples

### Basics

Pass `items` and control the selection via `value` / `onValueChange`. Exactly one segment is always active — `onValueChange` never fires with an empty value.

```tsx
import { useState } from "react";
import { SegmentedControl } from "@protocore/pds";

const RANGES = [
  { value: "1h", label: "1H" },
  { value: "24h", label: "24H" },
  { value: "7d", label: "7D" },
  { value: "30d", label: "30D" },
];

export default function Demo() {
  const [range, setRange] = useState("24h");

  return (
    <SegmentedControl
      items={RANGES}
      value={range}
      onValueChange={setRange}
      aria-label="Time range"
    />
  );
}
```

### Icon segments

Segments accept any `ReactNode` label. For icon-only segments, give each icon an `aria-label` and the control an `aria-label` for the group.

```tsx
import { useState } from "react";
import { SegmentedControl } from "@protocore/pds";
import { LayoutGrid, List, Table } from "lucide-react";

const VIEWS = [
  { value: "grid", label: <LayoutGrid size={15} aria-label="Grid" /> },
  { value: "list", label: <List size={15} aria-label="List" /> },
  { value: "table", label: <Table size={15} aria-label="Table" /> },
];

export default function Demo() {
  const [view, setView] = useState("grid");

  return (
    <SegmentedControl items={VIEWS} value={view} onValueChange={setView} aria-label="Layout" />
  );
}
```

## Do & don't

**Do**

- Keep to 2–4 segments with short, parallel labels.
- Give the control an `aria-label`, especially with icon-only segments.
- Use it to switch a view or scope that updates immediately.
- Keep exactly one segment meaningful as the default selection.

**Don't**

- Don't exceed ~4 segments — the row gets cramped; use Select or RadioGroup.
- Don't use long or wrapping labels; a segment is not a menu item.
- Don't use it for multi-select — that's a FilterBar or Checkbox set.
- Don't use it for a binary on/off setting — that's a Switch.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move focus into the group, landing on the active segment. |
| `Arrow Left / Right` | Move to and select the previous / next enabled segment. |
| `Home / End` | Move to and select the first / last enabled segment. |
| `Space / Enter` | Select the focused segment. |

**Notes**

- Built on Radix ToggleGroup (`type="single"`) with radiogroup semantics and roving tabindex — one Tab stop.
- Disabled segments are skipped during arrow navigation.
- Icon-only segments must carry their own label; the control needs an `aria-label` for the group name.

## Related

`radio-group`, `select`, `switch`, `chip`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
