Tabs Component

Create accessible tabbed interfaces with proper ARIA roles, keyboard navigation, and screen reader support.

intermediate
4.1.2 Name, Role, Value (Level A)2.4.3 Focus Order (Level A)3.2.1 On Focus (Level A)
โŒ

The Bad (Inaccessible)

<div class="tabs">
  <div class="tab" onclick="showTab(1)">Tab 1</div>
  <div class="tab" onclick="showTab(2)">Tab 2</div>
</div>
<div class="tab-content" id="content-1">...</div>
โœ…

Accessibility-Ready Code

<!-- Gold Standard: Proper tablist with roles -->
<div class="tabs-component">
  <div role="tablist" aria-label="Product features">
    <!-- Tab 1 -->
    <button
      type="button"
      role="tab"
      id="tab-1"
      aria-selected="true"
      aria-controls="panel-1"
      tabindex="0"
      class="tab-button"
    >
      Overview
    </button>

    <!-- Tab 2 -->
    <button
      type="button"
      role="tab"
      id="tab-2"
      aria-selected="false"
      aria-controls="panel-2"
      tabindex="-1"
      class="tab-button"
    >
      Features
    </button>

    <!-- Tab 3 -->
    <button
      type="button"
      role="tab"
      id="tab-3"
      aria-selected="false"
      aria-controls="panel-3"
      tabindex="-1"
      class="tab-button"
    >
      Pricing
    </button>
  </div>

  <!-- Tab Panels -->
  <div class="tab-panels">
    <div
      id="panel-1"
      role="tabpanel"
      aria-labelledby="tab-1"
      tabindex="0"
      class="tab-panel"
    >
      <h2>Overview</h2>
      <p>Product overview content...</p>
    </div>

    <div
      id="panel-2"
      role="tabpanel"
      aria-labelledby="tab-2"
      tabindex="0"
      class="tab-panel"
      hidden
    >
      <h2>Features</h2>
      <p>Features list...</p>
    </div>

    <div
      id="panel-3"
      role="tabpanel"
      aria-labelledby="tab-3"
      tabindex="0"
      class="tab-panel"
      hidden
    >
      <h2>Pricing</h2>
      <p>Pricing information...</p>
    </div>
  </div>
</div>

<script>
  const tabs = document.querySelectorAll('[role="tab"]');
  const panels = document.querySelectorAll('[role="tabpanel"]');

  tabs.forEach((tab, index) => {
    tab.addEventListener('click', () => activateTab(tab));

    tab.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowRight') {
        e.preventDefault();
        tabs[index + 1]?.focus() || tabs[0].focus();
      } else if (e.key === 'ArrowLeft') {
        e.preventDefault();
        tabs[index - 1]?.focus() || tabs[tabs.length - 1].focus();
      }
    });
  });

  function activateTab(selectedTab) {
    tabs.forEach(t => {
      t.setAttribute('aria-selected', 'false');
      t.setAttribute('tabindex', '-1');
    });
    panels.forEach(p => p.hidden = true);

    selectedTab.setAttribute('aria-selected', 'true');
    selectedTab.setAttribute('tabindex', '0');

    const panel = document.getElementById(
      selectedTab.getAttribute('aria-controls')
    );
    panel.hidden = false;
    panel.focus();
  }
</script>

The Standard

Tabs organize content into separate panels where only one is visible at a time. Proper ARIA roles and keyboard navigation are essential for accessibility.

WCAG Criteria

  • 4.1.2 Name, Role, Value: Proper ARIA roles.
  • 2.4.3 Focus Order: Logical tab order.
  • 3.2.1 On Focus: No focus changes without user action.

โŒ The Bad (Inaccessible)

Whatโ€™s Wrong?

  1. Using div with onclick: Not keyboard accessible.
  2. No tab/panel roles: Screen reader canโ€™t identify tabs.
  3. Forgetting aria-selected: Screen reader doesnโ€™t know active tab.
  4. No arrow key support: Poor keyboard UX.

โœ… The Good (Accessibility-Ready Code)

Why This Works

  1. Proper Roles: role="tablist", role="tab", role="tabpanel".
  2. Dynamic aria-selected: Updates on activation.
  3. tabindex Management: Only active tab is tabindex="0".
  4. Keyboard Navigation: Arrow keys move between tabs.

Accessibility Checklist

  • Use <div role="tablist"> for the tab container.
  • Use <button role="tab"> for each tab button.
  • Add aria-selected that updates dynamically.
  • Manage tabindex: active=0, inactive=-1.
  • Use <div role="tabpanel"> for content panels.
  • Link panels to tabs with aria-labelledby.
  • Add aria-controls on tabs pointing to panel IDs.
  • Support arrow keys for navigation.
๐Ÿ“ Perfect Implementation Reference
<div role="tablist" aria-label="Product features">
  <button
    type="button"
    role="tab"
    aria-selected="true"
    aria-controls="panel-1"
    tabindex="0"
  >
    Overview
  </button>
  <button
    type="button"
    role="tab"
    aria-selected="false"
    aria-controls="panel-2"
    tabindex="-1"
  >
    Features
  </button>
</div>

<div
  id="panel-1"
  role="tabpanel"
  aria-labelledby="tab-1"
  tabindex="0"
>
  <h2>Overview</h2>
</div>

<div
  id="panel-2"
  role="tabpanel"
  aria-labelledby="tab-2"
  tabindex="0"
  hidden
>
  <h2>Features</h2>
</div>

<script>
  tabs.forEach(tab => {
    tab.addEventListener('click', () => activateTab(tab));
    tab.addEventListener('keydown', handleArrowKeys);
  });
</script>

Technical Deep Dive

Screen Reader Announcements

  • NVDA: โ€œOverview, tab, 1 of 3, selectedโ€
  • VoiceOver: โ€œOverview, tab, 1 of 3, selectedโ€
  • JAWS: โ€œOverview, tab, 1 of 3, selectedโ€

Best Practice: Tab vs. Accordion

Use tabs when content is related but users will only need to view one section at a time, like different product features or settings categories. Use accordions when each section can be independently expanded/collapsed, like in a FAQ where users might want multiple sections open simultaneously.

๐Ÿงช

Interactive Behavioral Lab

๐Ÿ’ป

Interactive Sandbox

LIVE PREVIEW

๐Ÿ”ฌ Technical Internals

Understand how this component is processed by the browser and Assistive Technology (AT). This section bridges the gap between visual code and the hidden logic that powers accessibility.

๐ŸŒฒ Accessibility Tree

The data structure used by screen readers to "see" your page. It translates HTML roles and attributes into standardized objects.

โš™๏ธ Event Logic

Expected behavioral standards for keyboard navigation and state transitions. Crucial for users who don't use a mouse.

  • Focus: Highlights via :focus-visible
  • Activation: Responds to Enter and Space
  • Role: Identified as Tabs

๐ŸŒ Browser & Screen Reader Compatibility

Browser
Screen Reader
Status
๐ŸŽ Safari
VoiceOver
โœ“ Safe
๐ŸชŸ Chrome
NVDA
โœ“ Safe
๐ŸชŸ Edge
JAWS
โœ“ Safe
๐ŸฆŠ Firefox
NVDA
โœ“ Safe
๐Ÿ“ฑ iOS Safari
VoiceOver
โœ“ Safe
๐Ÿค– Chrome Android
TalkBack
โœ“ Safe