02UI

Tabs

Tabs show panels of related content one at a time, switched by clicking a label instead of loading a new page. That single page is the whole trade: nothing to bookmark, nothing to reload, unless you build the URL back in.

Structure and navigationBuild difficulty: Medium<div role="tablist">

Also called Tab bar, Tab strip, Segmented control.

Tabs trade a set of separate pages for one page that changes what it shows. That trade only pays off when every panel is a peer of the others, equally worth reaching, because a tab strip presents all its options as if they cost the same click.

Anatomy of tabs

Anatomy of tabs: the tab list holding every trigger, one trigger, the active indicator under the current trigger, the label text inside it, and the panel of content below.
  1. 1Tab list. The row holding every trigger, marked role="tablist" with an accessible name of its own.
  2. 2Trigger. One clickable label per panel, marked role="tab" with aria-selected reflecting whether it's current.
  3. 3Active indicator. The underline or filled background marking the current trigger. Needs 3:1 contrast against the strip (WCAG 1.4.11).
  4. 4Label. Short, scannable text. Every label should read at roughly the same length as its neighbours.
  5. 5Panel. The content for the active tab, marked role="tabpanel" and labelled by the trigger that opens it.

The list of triggers sits above the content it controls, and only one trigger is ever marked active at a time. The active indicator has to be visible enough to answer "where am I" at a glance, because the panel underneath changes completely and gives no other clue.

Here it is working. Click through the labels and watch the indicator and the panel move together.

A lightweight aluminium frame with a 10 hour battery, shipped in three colours.

Click through the labels. The active indicator and the panel below move together.

When to use tabs

Use tabs when

  • The panels are peers, like Overview, Specs and Reviews on one product
  • Only one panel needs attention at a time
  • There are roughly 2 to 7 panels, few enough to sit in one row without wrapping
  • Switching panels should feel instant, with no page reload

Reach for something else when

Variants

Segmented for a small fixed set, underline for a strip above a large content area, counts where size is worth knowing in advance.

Segmented. Triggers sit inside a rounded, filled track, and the active one gets its own background. Reads as a single control, which suits a small, fixed set like a view switcher.

Underline. A thin line marks the active trigger against a plain background. Reads lighter and suits a strip that sits above a large content area, like a settings page.

With counts. A small number beside a label, for panels whose size is worth knowing before you open them, like "Comments (12)".

States

Active
Inactive
Focus
Disabled
StateWhat changesWatch out for
ActiveIndicator and stronger text mark the current tabContrast between active and inactive text, not colour alone
InactiveMuted text, no indicatorStill fully clickable, never dimmed to the point of looking disabled
FocusA visible ring appears on the focused triggerDistinct from the active indicator, since a tab can be focused without being active while arrowing through the strip
DisabledReduced opacity, not reachable by keyboardState why nearby, the same as any other disabled control

Focus and active look similar enough to blur together if the ring is too subtle. Arrowing across a tab strip moves focus one trigger at a time without changing the active panel in some implementations, so the two states need to read as clearly different things.

How tabs behave

Only one panel renders at a time. The others unmount or stay hidden, which is what makes tabs fast to switch and unsuitable for content someone needs to compare side by side.

Selecting a tab is immediate. No confirm step, no separate button. Clicking or arrowing to a trigger and pressing Enter shows that panel right away.

The strip doesn't reorder itself. Tabs stay in the same position every time the page loads, even if usage data says one panel gets clicked more than the others. A jumping tab order breaks the spatial memory tabs are meant to build.

Content below the strip can change height. Unless every panel is padded to match the tallest one, switching tabs shifts whatever sits below them on the page. Reserve a minimum height if that shift is disruptive.

Tabs accessibility

Keyboard

KeyWhat happens
TabMoves focus into the strip, landing on the active trigger, then out to the first focusable element in the panel
Arrow left / Arrow rightMoves focus between triggers in a horizontal strip (Arrow up / Arrow down for a vertical one)
Home / EndJumps focus to the first or last trigger
Enter or SpaceActivates the focused trigger, if it isn't already active on arrow

The rest

  • Follow the WAI-ARIA tabs pattern. role="tablist" on the strip, role="tab" on each trigger with aria-selected, and role="tabpanel" on the content, connected with matching aria-controls and aria-labelledby.
  • Only the active tab sits in the normal tab order. The rest are reachable by arrow keys once focus enters the strip, following the same single-tab-stop pattern as a radio group.
  • Give the tablist an accessible name with aria-label, so a screen reader user knows what the group of tabs is for before hearing the individual labels.
  • Contrast. The active indicator needs 3:1 against the background it sits on (WCAG 1.4.11), the same bar as any other non-text UI indicator.
  • Target size. 24 by 24 CSS pixels minimum at Level AA (WCAG 2.5.8), which usually means padding a short label rather than shrinking the strip to fit its text.

What tabs should say

Labels are short nouns. "Reviews", not "See what people are saying". A tab strip is meant to be read in one glance across its full width.

Keep every label roughly the same length. One long label beside four short ones breaks the strip's rhythm and makes the odd one out look separate from the group.

Order by what people need first. Overview before Specs before Reviews, not alphabetical. The default tab should be the one most visits actually want.

Common tabs mistakes

Do

Three peer panels, none of them depends on the others being filled in first.

Don't

A checkout sequence, not a set of peers. Numbering the labels admits it. A stepper enforces the order this component only suggests.

The one that causes real support tickets is a required field sitting on a tab nobody opened, so the form fails to submit with no visible reason why.

1. A required field hidden on an unselected tab. The validation error has nothing to attach to on screen. Either validate before allowing a tab switch away from an incomplete panel, or route the person back to the tab that holds the problem.

2. Tabs used for a linear, step-by-step flow. Checkout steps aren't peers, they're a sequence, and a tab strip implies any of them can be opened in any order. That's a stepper's job, not this component's.

3. More tabs than fit in one row. A wrapped or horizontally scrolling strip past 7 or so items costs more attention than it saves. Group the overflow under a select or move it to a side navigation.

4. No URL sync for content worth sharing. A reader who copies the page's link and sends it lands back on the first tab, not the one they meant to show.

5. An active state that's only a colour change. Someone with low vision or colour blindness needs a second signal, like an underline or a filled background, not text colour alone.

6. Reordering tabs based on usage. Moving "Reviews" ahead of "Specs" because it gets clicked more breaks the spatial memory a returning visitor built on the last ten visits.

How to build tabs

<div role="tablist" aria-label="Product details">
  <button role="tab" id="tab-overview" aria-selected="true" aria-controls="panel-overview">
    Overview
  </button>
  <button role="tab" id="tab-specs" aria-selected="false" aria-controls="panel-specs" tabindex="-1">
    Specs
  </button>
  <button role="tab" id="tab-reviews" aria-selected="false" aria-controls="panel-reviews" tabindex="-1">
    Reviews
  </button>
</div>

<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">
  <!-- Overview content -->
</div>
<div role="tabpanel" id="panel-specs" aria-labelledby="tab-specs" hidden>
  <!-- Specs content -->
</div>
<div role="tabpanel" id="panel-reviews" aria-labelledby="tab-reviews" hidden>
  <!-- Reviews content -->
</div>

Arrow keys move focus between triggers and update aria-selected and tabindex, so only the active trigger sits in the page's normal tab order. Panels toggle hidden to match, and each one is labelled by the trigger that opens it rather than repeating the label as a heading.

Edge cases

A panel with no content yet. Show a real empty state inside the panel rather than leaving it blank, so switching to it doesn't read like a bug.

Deep linking to one tab. Read the active tab from a query parameter or route segment on load, and write it back on every switch, so a shared link and the browser back button both land on the right panel.

A label too long for its space. Truncate with an ellipsis and keep the full label available on focus or hover, never shortened so far it stops being distinct from its neighbours.

Nested tabs. Two tab strips stacked, one controlling the other's panels, is rare and usually a sign the content needs a different structure, like a sidebar plus tabs, rather than tabs inside tabs.

Tabs in real products

Stripe's dashboard uses an underline tab strip across the top of a settings page, with the URL synced to each tab so a support link can point straight at "Webhooks" instead of "Settings".

GitHub's repository page uses tabs for Code, Issues, Pull requests and more, each one a real page with its own URL rather than a client-side panel swap, so every tab is bookmarkable and indexable on its own.

Linear's issue view uses a segmented control to switch between "Activity" and "History" on the same issue, a small, fixed set where the contained pill style reads as one control rather than a page section.

Common questions

Tabs or an accordion?
Tabs show one panel at a time and hide the rest completely, which suits content people pick between, like Overview versus Specs. An accordion keeps every section in the page flow and lets several open together, which suits content people scan in sequence, like an FAQ. On a narrow screen, three or four tabs also cost more taps to compare than the same sections stacked as an accordion.
How many tabs is too many?
Past about 7, a single row starts wrapping or scrolling, and both cost more attention than the tabs saved. A wrapped row breaks the one-line scan tabs exist for. Past that count, a select or a side navigation menu handles the same choice without fighting the layout.
Should the URL change when someone switches tabs?
For anything worth bookmarking, sharing or reaching with a browser back button, yes. Sync the active tab to a query parameter or route segment. For a short-lived UI state, like which chart series is showing inside a widget, it usually doesn't need to survive a reload and the URL can stay put.
Can a tab hold a form field?
Only if nothing outside that tab depends on it. A required field hidden behind an unselected tab can block a form submission with no visible error, because the invalid field is off-screen. If a field must be filled, either put it on the tab that's selected by default or validate per tab before allowing a submit.
Do tabs work on mobile?
A row of 2 to 4 short labels does. Past that, either scroll the tab strip horizontally with a clear affordance that more exist, or switch to a select or accordion, both of which handle narrow widths without truncating labels.

Last reviewed 29 JUL 2026