Tabs
Switches between peer views in the same context — Overview / Activity /
Settings. Built on Base UI Tabs: the active tab renders as the inverted chip
(photon.codes’ Tab), with an optional Indicator that slides between tabs and
full arrow-key navigation.
Import
import { Tabs } from "@photon-ai/pho-ui/components/tabs";
Basic
Each Tabs.Tab and its Tabs.Panel share a value. The active tab is the
inverted chip; drop a Tabs.Indicator in the list for a sliding highlight.
Leave it uncontrolled with defaultValue.
<Tabs.Root defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="activity">Activity</Tabs.Tab>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Panel value="overview">…</Tabs.Panel>
<Tabs.Panel value="activity">…</Tabs.Panel>
</Tabs.Root>
Controlled
Pass value and onValueChange to own the active tab in your own state — for
deep-linking a tab to the URL, syncing two tab bars, or driving selection from
elsewhere in the UI.
const [value, setValue] = useState("overview");
<Tabs.Root value={value} onValueChange={setValue}>
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="activity">Activity</Tabs.Tab>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Panel value="overview">…</Tabs.Panel>
<Tabs.Panel value="activity">…</Tabs.Panel>
</Tabs.Root>;
Anatomy
- Tabs.Root — owns the active
value(value/defaultValue) - Tabs.List — the tab bar; positions the indicator (
role="tablist") - Tabs.Tab — one tab, identified by
value(role="tab") - Tabs.Indicator — optional chip highlight that slides to the active tab
- Tabs.Panel — content for the matching
value(role="tabpanel")
Props
Each part forwards to its Base UI counterpart and accepts that element’s native
props plus className. The load-bearing props:
Tabs.Root
- value —
string— active tab when controlled - defaultValue —
string— initial selected tab; omit it and no tab starts selected, since Base UI’s default of0matches none of these string values - onValueChange —
(value, eventDetails) => void— fires on selection - orientation —
horizontal·vertical(defaulthorizontal; the pho list and indicator styling target horizontal)
Tabs.List
- activateOnFocus —
boolean— switch panels as arrow keys move focus (defaultfalse) - loopFocus —
boolean— wrap arrow-key focus past the ends (defaulttrue)
Tabs.Tab
- value —
string(required) — pairs the tab with its panel - disabled —
boolean— non-interactive tab (defaultfalse)
Tabs.Panel
- value —
string(required) — panel shown for the matching tab - keepMounted —
boolean— keep the panel mounted while hidden (defaultfalse)
Accessibility
Tabs.List is a role="tablist", each Tabs.Tab a role="tab" carrying
aria-selected and aria-controls for its panel, and each Tabs.Panel a
focusable role="tabpanel" labelled by its tab. A roving tabindex keeps only
the active tab in the page’s tab order.
Keyboard:
- Left / Right arrows move focus between tabs (Up / Down when
orientation="vertical"), wrapping past the ends by default. - Home / End jump to the first / last tab.
- Enter / Space activate the focused tab. Because
activateOnFocusdefaults tofalse, arrowing does not switch panels until you press one; set it onTabs.Listto activate on focus instead. - Tab moves focus off the active tab and into its panel.
Focus is shown with the pho-brand focus-visible ring on both tabs and panels.
There is no live region — selection is conveyed through aria-selected.
Best practices
- Use tabs for peer views of the same subject, not for steps in a sequence — for ordered steps, use a stepper or separate pages.
- Keep labels to one or two words so the bar stays scannable; if you need many tabs, reconsider the information architecture.
- Panels unmount when inactive by default. Set
keepMountedon aTabs.Panelto preserve form input or expensive content across tab switches. - Don’t nest tab bars within tab bars — it’s disorienting and the indicators compete.