Skip to main content

What it is

A tab bar switches between sections of a single subject, in place, without leaving the page. It is page furniture: it belongs to the header and it changes what fills the body beneath it. It is not navigation between different subjects, and it is not a filter. If the sections have different subjects it is navigation; if they are the same records narrowed differently it is filtering.

Live example

Exports

Vocabulary

“Tabs” names opposite things on the two sides of the system. The design library calls the container a Tab Bar and each item Tabs. Code calls the container Tabs and each item Tab.In review, say tab bar for the container and tab for the item. Those are unambiguous in both directions, and this documentation uses them throughout.
Say “selected”, not “active”. A tab is selected; a session is active. The distinction matters because a page can have a selected tab whose contents are still loading, and “active” reads as a claim about the content rather than the control.

Choose Tabs when

  • One subject has two or more sections a reader moves between, and each section stands alone.
  • A reader needs one section at a time, and comparing sections side by side is not the job.
  • The sections are peers. No section is a step toward another.

Choose something else when

Anatomy

The bar draws its own bottom rule. That is why a header carrying a tab bar suppresses the page divider rather than drawing both — TITAN-VIEW-12.

Variants, sizes, and states

Orientation is the first decision, and the two are different components in everything but name. Horizontal is a tab bar. Vertical is a navigation rail — it has its own minimum row width, a selected row with a fill and a leading edge marker, a rule down its trailing side, and an indented mode for nested rows. This page documents the horizontal bar. What the rail is for is undecided — see TITAN-GAP-29. How the bar distributes its tabs — three published behaviours, and the system has not stated which is the default choice: What happens when the tabs do not fitvariant="scrollable" with three settings for the scroll affordance:
scrollButtons={false} produces a bar a pointer user cannot reach the end of. Overflowing tabs stay reachable by keyboard and by touch-drag, and a mouse user gets no arrows and no scrollbar. A published story demonstrates it, which is not the same as the system endorsing it. Use "auto"TITAN-TAB-06.
States a tab carries: unselected, selected, hover, pressed, focused, and disabled. The design library also defines an error display for a tab, across every interaction state. Code has no error treatment for a tab — verified against the theme overrides, the token set, the shared package, and the legacy stylesheets. See TITAN-GAP-30.

Edge and failure states

Tokens

Three component tokens exist, and all three serve the vertical rail: tabs-vertical-min-width, tabs-icon-gap, and tabs-indented-font-size. The horizontal bar carries no component tokens of its own — its rule comes from the border tokens and its label from the type scale, and its internal padding comes from no token at all.

Composition

The bar goes in the header. A tabbed page carries its bar in the header’s second row and suppresses the page divider; the header’s second row holds a tab bar or a filter row and never both. That is a view-tier decision and it is documented with the page frame, including the constraint that Header cannot hold the bar today (TITAN-DIV-16). Each tab needs a panel, and the pairing is manual. The shipped TabPanel defaults to naming a tab that nothing produces, so set the ids yourself on both sides:
See TITAN-DIV-14 for the pairing and TITAN-DIV-15 for what the shipped panel does to block content. Every application that needs a panel has written its own. The rail composes further. TabIconLabel gives a rail row an icon, a truncating label, and a trailing accessory; TabMenu gives a row its own overflow menu. A published story assembles a section header with expand and collapse, indented sub-rows, a status chip on a row, and per-row menus. That is a navigation pattern, not a component, and nothing states when to reach for it — see TITAN-GAP-29.

Content

  • A tab label names a section, not an action. “Routing”, not “Configure routing”.
  • One to three words. Labels do not truncate gracefully and the bar has finite width.
  • Sentence case — unconfirmed, not a settled rule. Capitalization and punctuation lists tab labels explicitly as an open gap, not a confirmed content type. Treat this as this page’s own best guess until that page’s gap closes.
  • No counts in labels unless the count is the point. “Members” is a section; “Members (147)” commits the label to staying accurate.
  • Never number the tabs. Numbering implies order, and tabs are peers. A sequence is a wizard.

Accessibility

The bar needs an accessible name. Give Tabs an aria-label describing what it divides — “Campaign sections”, not “Tabs”. Without it a screen-reader user hears a tab list with no indication of what it belongs to. Each tab and its panel reference each other. The tab carries id and aria-controls; the panel carries id and aria-labelledby pointing back. The frame does not wire this and the shipped panel wires it wrong — TITAN-DIV-14. Keyboard. The bar is a single tab stop. Arrow keys move between tabs, Home and End jump to the ends. A bar of nine sections costs one stop on the way to the body, not nine. A tab label is not a heading. The section a tab reveals still needs its own heading — a reader outlining the page by heading level does not see tab labels. An icon-only tab must carry an explicit accessible name. A published story does this with aria-label on each tab. Without it the tab announces as “tab” and nothing else. A disabled tab is not focusable and announces nothing. A section a reader cannot open, with no stated reason, is indistinguishable from a bug.

Constraints

Known issues

Tabs: open issues

Divergences, open decisions, and undocumented gaps for Tabs.

Why it works this way

The bar belongs to the header, not to the body. A section is part of what the page is, so the control that switches sections sits with the page’s identity. A bar floating between two regions has to be re-justified on every page. This is the view tier’s decision and the reasoning is recorded with the page frame. Selection is addressable. A tab that changes only local state cannot be linked, cannot survive a reload, and is lost to the back button — and the shipped panel unmounts the section it leaves, so unsaved work in it goes too. Making each section a URL turns a tab bar into navigation the browser understands (TITAN-VIEW-14). Peers, not steps. The strongest reason to keep filters and sequences out of a tab bar is that both imply the sections are not peers — a filter says some of these are subsets, a step says one comes before another. Once either is in the bar, a reader can no longer tell what a tab means, and every other rule on this page is arguing about something that has already been lost.

Status and changelog

Last modified on September 2, 2026