Tabs & Toolbars

Tabs

Available

Switches content within the same area. Uses arrow key navigation, aria-selected, and roving tabindex.

What Are Tabs?

Tabs are a UI pattern that layers multiple panels of content in the same area, letting users switch which panel is visible by selecting a tab label at the top. They are commonly used in settings screens, product details, and dashboards — anywhere you want to present related information while saving space.

Visually, tabs may look like a row of buttons, but assistive technologies need to know"this is a group of tabs and which one is currently selected."

Why Does Accessibility Matter?

The two key techniques are "one tab stop for the entire tab group (roving tabindex)" and"conveying the selection state with aria-selected."

Live Demo (Recommended Implementation)

The tabs below follow the APG pattern. Try operating them without a mouse, using only the keyboard.

Accessible Tabs

Edit your public profile, such as display name and avatar.

Try it: Tab into the selected tab → use ← → to move between tabs (the panel switches simultaneously) → Home / End to jump to the first or last tab → Tab again to move into the panel.

Tip

When a screen reader focuses on a tab, it announces something like "Profile, tab, selected, 1 of 3" — conveying the role, selection state, and position. This is the effect ofrole="tab" and aria-selected.

Keyboard Interaction

KeyActionPriority
TabEnter / exit the tab list (the entire list is a single tab stop)Required
/ Move to the previous / next tab (with automatic activation, also selects the tab)Required
Home / EndMove to the first / last tabOptional
Enter / SpaceSelect the focused tab in manual activation modeOptional

Note

If the panel content is lightweight and can be displayed instantly, automatic activation— where pressing an arrow key both moves focus and selects the tab — provides the best experience (this demo uses this approach). If displaying the panel requires a network request or heavy processing, use manual activation, where Enter/Space confirms the selection.

Required WAI-ARIA Roles and Properties

TargetAttribute / RoleMeaning
Tab list containerrole="tablist" + aria-labelConveys that the element is a collection of tabs and provides its accessible name.
Each tabrole="tab"Identifies the element as a tab. Using <button> as the base element is recommended.
Each tabaria-selected="true | false"Indicates which tab is currently selected. Must be updated on every tab switch.
Each tabaria-controls="<panel id>"Points to the panel controlled by this tab.
Each tabtabindex="0 | -1"Set to 0 for the selected tab and -1 for all others (roving tabindex).
Each panelrole="tabpanel" + aria-labelledby="<tab id>"A panel labeled by its corresponding tab.
Each paneltabindex="0"Ensures the panel itself is focusable via Tab even when it contains no focusable elements.
Hidden panelshiddenHides unselected panels from both screen readers and interaction.

Implementation: Recommended Pattern (Good)

Good / Recommended

Tabs use <button role="tab">, and panels use role="tabpanel". Keep the selection state in sync via aria-selected and roving tabindex.

Markup:

<div class="tabs">
  <div role="tablist" aria-label="Account settings">
    <button type="button"
            role="tab"
            id="tabs-tab-profile"
            aria-selected="true"
            aria-controls="tabs-panel-profile"
            tabindex="0">Profile</button>
    <button type="button"
            role="tab"
            id="tabs-tab-security"
            aria-selected="false"
            aria-controls="tabs-panel-security"
            tabindex="-1">Security</button>
  </div>

  <div role="tabpanel"
       id="tabs-panel-profile"
       aria-labelledby="tabs-tab-profile"
       tabindex="0">Profile content…</div>
  <div role="tabpanel"
       id="tabs-panel-security"
       aria-labelledby="tabs-tab-security"
       tabindex="0"
       hidden>Security content…</div>
</div>

Switching script (syncs aria-selected, tabindex, and hidden in one place):

document.querySelectorAll('[data-tabs]').forEach((root) => {
  const tabs = Array.from(root.querySelectorAll('[role="tab"]'));

  function select(tab) {
    tabs.forEach((t) => {
      const selected = t === tab;
      // Keep aria-selected and roving tabindex in sync
      t.setAttribute('aria-selected', String(selected));
      t.tabIndex = selected ? 0 : -1; // Only the selected tab is reachable via Tab
      const panel = document.getElementById(t.getAttribute('aria-controls'));
      if (panel) panel.hidden = !selected;
    });
  }

  tabs.forEach((tab, i) => {
    tab.addEventListener('click', () => select(tab));
    tab.addEventListener('keydown', (e) => {
      let next = null;
      if (e.key === 'ArrowRight') next = (i + 1) % tabs.length;
      else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
      else if (e.key === 'Home') next = 0;
      else if (e.key === 'End') next = tabs.length - 1;
      if (next !== null) {
        e.preventDefault();
        tabs[next].focus(); // Move focus
        select(tabs[next]); // Select the tab at the same time (automatic activation)
      }
    });
  });
});

Anti-pattern (Bad)

The example below uses only <div> elements and onclick handlers.It switches with a mouse, but is completely inoperable via keyboard.

Broken tabs built with divs
Profile
Security
Notifications

Edit your public profile, such as display name and avatar.

Try it: pressing Tab does not focus any tab, and ← → does not switch between them. A screen reader cannot identify them as 'tabs' or determine which is 'selected.'

<!-- ❌ Anti-pattern -->
<div class="tabs">
  <div class="tablist">
    <!-- div + onclick. No role, no aria-selected, no keyboard support -->
    <div class="tab" onclick="show('p1')">Profile</div>
    <div class="tab" onclick="show('p2')">Security</div>
  </div>
  <div id="p1">Profile content…</div>
  <div id="p2" style="display:none">Security content…</div>
</div>

Bad / Avoid

Problems with this implementation:

  • Not keyboard operablediv elements cannot receive focus, and arrow keys have no effect.
  • Not recognized as tabs — without role="tablist"/role="tab", they sound like plain text.
  • Selection state not conveyed — without aria-selected, there is no way to tell which tab is active.
  • Panels not associated — without aria-controls / aria-labelledby, the relationship between tabs and panels is lost.

Implementation Checklist


Source (English):Tabs Pattern — W3C APG(opens in a new tab)