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-hairlinehairline; the indicator rides it. - indicator
- A solid
--gold-aa-500underline 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-shadowhas no job here. The weight does NOT change either way, so the row never reflows when you switch tabs. - focus
- The gold
--focus-ringon 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
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.
Related
- /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.