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

- **Category:** Navigation (`navigation`)
- **Slug:** `navigation/tabs`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Tabs } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/navigation/tabs

> Underlined tab set — mono UPPERCASE triggers over a hairline rail, the active tab inking its label and growing a 2px underline.

## When to use it

Reach for **Tabs** to switch between peer views of the *same* subject inside a page — an entity's Overview / Config / Logs. It keeps one panel visible at a time and preserves the surrounding layout.

- Need a marketing-weight feature switcher with copy beside an illustration? Use **TabbedShowcase**.
- Two-to-four mutually exclusive *options* that filter a view (not navigate it)? Use **SegmentedControl**.
- Independent disclosure of stacked content where several can be open? Use **Accordion**.
- Moving between separate destinations/pages? That is navigation — use **Breadcrumb** or a nav, not Tabs.

## Examples

### Basics

Compose `Tabs.Root` (aliased as `Tabs`) with a `Tabs.List` of `Tabs.Trigger`s and one `Tabs.Content` per trigger `value`. Uncontrolled state lives in `defaultValue`.

```tsx
import { Tabs, Text } from "@protocore/pds";

export default function TabsBasics() {
  return (
    <Tabs.Root defaultValue="overview">
      <Tabs.List>
        <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
        <Tabs.Trigger value="validators">Validators</Tabs.Trigger>
        <Tabs.Trigger value="config">Config</Tabs.Trigger>
      </Tabs.List>
      <Tabs.Content value="overview">
        <Text>Mainnet endpoint is live across 42 chains, settling in a single hop.</Text>
      </Tabs.Content>
      <Tabs.Content value="validators">
        <Text>3 DVNs attest every message; the app sets its own threshold.</Text>
      </Tabs.Content>
      <Tabs.Content value="config">
        <Text>Executor prepays destination gas; retries are idempotent.</Text>
      </Tabs.Content>
    </Tabs.Root>
  );
}
```

### Controlled

Drive the active tab from your own state with `value` + `onValueChange` — useful when the URL, a wizard, or a query drives which panel shows.

```tsx
import { useState } from "react";
import { Tabs, Text, Badge } from "@protocore/pds";

const PANELS = {
  pending: "12 messages awaiting verification.",
  inflight: "4 messages in transit to the destination chain.",
  settled: "1,204 messages delivered in the last hour.",
};

export default function TabsControlled() {
  const [tab, setTab] = useState<keyof typeof PANELS>("inflight");
  return (
    <div>
      <Tabs.Root value={tab} onValueChange={(v) => setTab(v as keyof typeof PANELS)}>
        <Tabs.List>
          <Tabs.Trigger value="pending">Pending</Tabs.Trigger>
          <Tabs.Trigger value="inflight">In flight</Tabs.Trigger>
          <Tabs.Trigger value="settled">Settled</Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value={tab}>
          <Text>{PANELS[tab]}</Text>
        </Tabs.Content>
      </Tabs.Root>
      <div style={{ marginTop: "var(--pds-space-4)" }}>
        <Badge tone="info">active: {tab}</Badge>
      </div>
    </div>
  );
}
```

## Do & don't

**Do**

- Keep trigger labels to one or two words so the mono UPPERCASE row stays scannable.
- Give every Tabs.Content a value that matches exactly one Tabs.Trigger.
- Use Tabs for peer views of one subject; keep the page URL stable.
- Let Radix own focus — don't intercept arrow keys.

**Don't**

- Don't use Tabs to page through a sequence — that's Steps or Pagination.
- Don't hide required form fields behind an inactive tab where validation can't reach them.
- Don't nest Tabs inside Tabs; flatten the information architecture instead.
- Don't put more than ~5 triggers in one list; it stops reading as a rail.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move focus to the active trigger, then into the panel |
| `→ / ←` | Move to the next / previous trigger (roving focus, activates it) |
| `Home / End` | Move to the first / last trigger |
| `Space / Enter` | Activate the focused trigger |

**Notes**

- Built on Radix Tabs: the list is a `role="tablist"`, triggers are `role="tab"`, panels are `role="tabpanel"` and wired together via `aria-controls` / `aria-labelledby`.
- The active trigger carries `aria-selected` and `data-state="active"`; only the active panel is in the tab order.

## Related

`segmented-control`, `tabbed-showcase`, `accordion`, `breadcrumb`

---

© 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/
