Components

Tabs

In-content section navigation — the spine of a detail view (a Record's Overview · Data · History). Built on React Aria, so the tablist / tab / tabpanel roles, arrow-key navigation, roving tabindex, and focus management come from the library. We just bring the paint: the selected tab gets a full-strength label and a solid gold underline — flat and friendly, no engraving (that trick belongs to the proposed system).

Tabs

Arrow-key between tabs, Tab into the panel. The gold underline slides to the tab you pick; the active label goes full strength while the rest hang back.

The identity and the key facts — where you land first. A PageHeader hero sits above; this is the part it introduces.

The fourth tab is disabled — no archived records on this officer.

Materiality

Flat and quiet until you pick a tab — then the gold tells you where you are. Everything resolves to tokens.

rail
The list sits on a --border-hairline hairline; the indicator rides it.
indicator
A solid --gold-aa-500 underline that slides to the tab you pick. Its presence — not its color — is the selection cue, so it survives grayscale (ADR-0017).
selected label
Full-strength fg-1, flat — engraving belongs to the proposed system, so --engraving-shadow has no job here. The weight does NOT change either way, so the row never reflows when you switch tabs.
focus
The gold --focus-ring on keyboard focus (data-focus-visible), on both the tab and the panel.

Accessibility

  • Real tablist / tab / tabpanel. React Aria wires the roles, aria-selected, and aria-controls so screen readers announce the structure.
  • Arrow-key navigation. Left/Right (or Up/Down when vertical) move between tabs; Tab moves into the panel — roving tabindex, not a tab-stop per tab.
  • Selection is never colour alone. The indicator's presence + aria-selected carry it; the gold is reinforcement.

API

<Tabs>
Root. selectedKey / defaultSelectedKey / onSelectionChange (controlled or not), orientation, isDisabled, keyboardActivation.
<Tabs.List>
The tab row. Needs aria-label or aria-labelledby. Holds Tabs.Tab children.
<Tabs.Tab id>
One tab. id is its key (matches a panel); isDisabled greys it out.
<Tabs.Panel id>
The content for the tab whose id matches. Lives anywhere under Tabs.

Usage

A record's sectionstsx
import { Tabs } from '@halo-compliance/ui'

<Tabs defaultSelectedKey="overview">
  <Tabs.List aria-label="Officer sections">
    <Tabs.Tab id="overview">Overview</Tabs.Tab>
    <Tabs.Tab id="data">Data</Tabs.Tab>
    <Tabs.Tab id="history">History</Tabs.Tab>
  </Tabs.List>

  <Tabs.Panel id="overview">{/* the identity + key facts */}</Tabs.Panel>
  <Tabs.Panel id="data">{/* editable fields */}</Tabs.Panel>
  <Tabs.Panel id="history">{/* the timeline */}</Tabs.Panel>
</Tabs>

When to use

  • Sections of one record. Overview / Data / History on a detail view — peer views of the same entity.
  • Not for navigation between pages. Moving between records or routes is the rail or a link, not tabs — tabs stay within one surface.
  • Not for a sequence. A multi-step flow with an order is a wizard (Steps), not tabs the user free-roams.

Anti-patterns

  • Too many tabs. Past a handful, the row wraps or scrolls and stops being scannable — reconsider the information architecture.
  • Tabs as routes. If each "tab" is really a page (its own URL, back-button), use the router and the rail, not Tabs.
  • Hiding required actions in a tab. A primary action a user must take shouldn't live behind a tab they may never open.
  • /record — the detail view whose sections these are.
  • /page-header — the identity hero that sits above the tabs.
  • /modal — the overlay that shares React Aria's focus machinery.