Tabs switch between sibling views without leaving the page. Tab Bar is the bar and the tabs only: it draws them, marks one as current, and moves focus between them with the arrow keys. The panel underneath is yours.
Anatomy
Section titled “Anatomy”-
Tab bar the bar itself, holding the title, the tabs and the actions slot.ReportsReports -
Title (optional) names the set of tabs. It sits inside the bar, before the first tab.ReportsReports -
Active tab the view you are looking at, marked by the underline and nothing else — so nothing else in the bar should read as current.ReportsReports -
Inactive tab a sibling view, one click away.ReportsReports -
Badge (optional) a count on a tab.ReportsReportsbadgeandstickerare exclusive; a tab takes one or neither. -
Actions (optional) the slot at the end of the bar.ReportsReportsactionstakes any node — an add-tab button here, an overflow menu elsewhere.
When to use
Section titled “When to use”Use tabs for two to six views of the same object, where the reader will go back and forth between them and nothing is lost by hiding one. An account’s Overview, Verifications and Devices are tabs; the steps of a form are not, because a step has to be finished before the next one means anything.
Use Link for a destination that replaces the page rather than a region of it, and a Dropdown Menu for actions on the object the tabs belong to. A tab whose panel repeats another tab’s panel with one value changed is a filter, not a view, and belongs in the panel as a Button Group.
Tabs can drive the URL: as renders a tab as a link or a router link instead of
a button, and the styles for that are in the component. What makes it a tab is
still that the destination renders in the region below the bar.
Behavior
Section titled “Behavior”Mark exactly one tab as selected
Section titled “Mark exactly one tab as selected”selected is a prop on each tab, and nothing above them coordinates it. The
component does not clear the previous tab’s underline or hold a value of its
own, so two tabs passed selected both draw the underline and both report
aria-selected. The current view lives in your state, and the bar renders it.
The underline is the entire signal. Nothing else in the bar changes with selection, which is why nothing else in the bar should look emphasized.
A tab that does not fit is cut off, not moved
Section titled “A tab that does not fit is cut off, not moved”There is no overflow control: no scroller, no overflow menu, no affordance for a tab you cannot see. The tab row wraps, and the bar is a fixed 32px high with hidden overflow, so a tab that does not fit is cut off rather than moved somewhere reachable — present in the DOM, not on the screen. Decide the number of tabs against the narrowest place the bar will render, not against your own window.
Truncate by character count, or not at all
Section titled “Truncate by character count, or not at all”maxTabChars cuts each label to that many characters and appends an ellipsis.
It is a character count rather than a width, so it truncates identically in a
wide bar and a narrow one, and it only applies to a tab whose child is a plain
string — a tab holding markup is left at full length. Setting it also puts the
untruncated text in a title on every string tab, so hovering any tab in the
bar produces a browser tooltip, including the ones that were short enough.
Accessibility
Section titled “Accessibility”Wire the panel to the tab yourself
Section titled “Wire the panel to the tab yourself”The bar renders role="tablist" and each tab role="tab" with aria-selected.
The third role is not here: there is no panel component, so role="tabpanel",
its aria-labelledby, and the aria-controls on the tab pointing back at it
are yours to write. Two of the three roles agreeing is not the pattern — a tab
with no aria-controls announces itself as a tab belonging to nothing.
Every tab is its own tab stop
Section titled “Every tab is its own tab stop”The arrow keys move focus along the bar and wrap at both ends, and Home and End jump to the first and last; disabled tabs are skipped. No tab is removed from the tab order, though, so Tab walks through all of them one at a time rather than leaving the bar. Moving focus does not switch view — a tab activates on Enter, Space or click.
Move the bar’s heading outside the tablist
Section titled “Move the bar’s heading outside the tablist”title renders inside the tablist, before the first tab, as a div with no role.
The arrow keys count it as one of the stops, so with a title set, Left from the
first tab and Right from the last land on it and focus stays where it was
instead of wrapping. Name the bar with aria-label and put a visible heading
above it.
Content
Section titled “Content”Label a tab with the name of its view
Section titled “Label a tab with the name of its view”A tab label is the name of what the panel shows: “Verifications”, “Devices”, “Activity”. Tabs are read against each other, so the label only has to distinguish this view from its siblings, and a shared word in all of them — “Account details”, “Account devices” — is width spent on something the reader already knows.
A tab carries a count or a label beside its text, never both: badge and
sticker are mutually exclusive in the type. A Badge is
recolored to match the tab’s selected state, so a count reads as part of the
tab; a Sticker is rendered exactly as you pass it, which
is what makes it the one to use for “Beta” and other labels that should not
change with selection.
| Name | Type |
|---|---|
actions |
|
title |
|
data-tour-target |
|
maxTabChars |
Truncate tab text to the specified number of characters. |
size |
Size of the tabs. Affects font size. |
ComponentPropsWithoutRef<‘div’>.Was this page helpful?