Photon Design System

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.

A summary of your project — deployments, domains, and usage at a glance.
<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.

A summary of your project — deployments, domains, and usage at a glance.
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 of 0 matches none of these string values
  • onValueChange — (value, eventDetails) => void — fires on selection
  • orientation — horizontal · vertical (default horizontal; the pho-ui list and indicator styling target horizontal)

Tabs.List

  • activateOnFocus — boolean — switch panels as arrow keys move focus (default false)
  • loopFocus — boolean — wrap arrow-key focus past the ends (default true)

Tabs.Tab

  • value — string (required) — pairs the tab with its panel
  • disabled — boolean — non-interactive tab (default false)

Tabs.Panel

  • value — string (required) — panel shown for the matching tab
  • keepMounted — boolean — keep the panel mounted while hidden (default false)

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 activateOnFocus defaults to false, arrowing does not switch panels until you press one; set it on Tabs.List to 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 keepMounted on a Tabs.Panel to preserve form input or expensive content across tab switches.
  • Don’t nest tab bars within tab bars — it’s disorienting and the indicators compete.