Skip to content

ARIA Project — Build an Accessible Tab Panel

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about ARIA Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Apply all ARIA concepts to build a complete accessible tab panel with roles, states, keyboard navigation, roving tabindex, aria-controls, and aria-selected.

In this project tutorial, you'll build a production-ready ARIA tab panel component.

What You'll Learn

By the end of this project, you'll have built a complete, tested, and accessible tab panel that meets all ARIA Authoring Practices Guide requirements.

Why It Matters

Tab panels are one of the most common ARIA patterns. Mastering this pattern prepares you for all other Composite widget implementations.

Real-World Use

Doda Browser's settings panels and Durga Antivirus Pro's results dashboard both use this exact ARIA tab pattern.

Project Architecture

flowchart TD
  A[tablist] --> B[tab 1]
  A --> C[tab 2]
  A --> D[tab 3]
  B --> E[aria-controls='panel-1']
  C --> F[aria-controls='panel-2']
  D --> G[aria-controls='panel-3']
  B --> H[aria-selected='true']
  C --> I[aria-selected='false']
  D --> J[aria-selected='false']
  K[tabpanel id='panel-1'] --> L[Visible content]
  M[tabpanel id='panel-2'] --> N[Hidden]
  O[tabpanel id='panel-3'] --> P[Hidden]

HTML Structure

<div class="tabs">
  <div role="tablist" aria-label="Scan settings" class="tab-list">
    <button role="tab"
            aria-selected="true"
            aria-controls="panel-quick"
            id="tab-quick"
            tabindex="0">
      Quick Scan
    </button>
    <button role="tab"
            aria-selected="false"
            aria-controls="panel-full"
            id="tab-full"
            tabindex="-1">
      Full Scan
    </button>
    <button role="tab"
            aria-selected="false"
            aria-controls="panel-custom"
            id="tab-custom"
            tabindex="-1">
      Custom Scan
    </button>
  </div>

  <div role="tabpanel" id="panel-quick" aria-labelledby="tab-quick">
    <h3>Quick Scan Settings</h3>
    <p>Scans common threat locations.</p>
  </div>

  <div role="tabpanel" id="panel-full" aria-labelledby="tab-full" hidden>
    <h3>Full Scan Settings</h3>
    <p>Scans entire system.</p>
  </div>

  <div role="tabpanel" id="panel-custom" aria-labelledby="tab-custom" hidden>
    <h3>Custom Scan Settings</h3>
    <p>Select specific folders to scan.</p>
  </div>
</div>

JavaScript Implementation

class TabPanel {
  constructor(container) {
    this.container = container;
    this.tablist = container.querySelector('[role="tablist"]');
    this.tabs = Array.from(this.tablist.querySelectorAll('[role="tab"]'));
    this.panels = this.tabs.map(tab =>
      document.getElementById(tab.getAttribute('aria-controls'))
    );

    this.tablist.addEventListener('keydown', (e) => this.handleKeydown(e));
    this.tabs.forEach(tab => {
      tab.addEventListener('click', () => this.selectTab(tab));
    });
  }

  selectTab(selectedTab) {
    this.tabs.forEach((tab, i) => {
      const isSelected = tab === selectedTab;
      tab.setAttribute('aria-selected', isSelected);
      tab.setAttribute('tabindex', isSelected ? '0' : '-1');
      this.panels[i].hidden = !isSelected;
    });
    selectedTab.focus();
  }

  handleKeydown(e) {
    const currentIndex = this.tabs.findIndex(t => t === document.activeElement);
    let newIndex;

    switch (e.key) {
      case 'ArrowRight':
        newIndex = (currentIndex + 1) % this.tabs.length;
        break;
      case 'ArrowLeft':
        newIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length;
        break;
      case 'Home':
        newIndex = 0;
        break;
      case 'End':
        newIndex = this.tabs.length - 1;
        break;
      default:
        return;
    }

    e.preventDefault();
    this.selectTab(this.tabs[newIndex]);
  }
}

new TabPanel(document.querySelector('.tabs'));

Testing Checklist

  • Tablist has aria-label
  • Tabs have aria-selected, aria-controls, tabindex
  • Only one tab has tabindex="0"
  • Tabpanels have aria-labelledby matching tab ID
  • Arrow keys navigate between tabs
  • Home/End navigate to first/last tab
  • Tab moves focus into the active tabpanel
  • Selected tabpanel is visible, others are hidden

Common Mistakes

  • Missing aria-labelledby on tabpanels: The panel must reference its tab for name.
  • Not hiding inactive panels: Use the hidden attribute or JavaScript visibility control.
  • Incorrect arrow key direction: Left/Right for horizontal tabs, Up/Down for vertical.
  • Not handling Home and End keys: Users expect these to navigate to boundaries.
  • Forgetting to update tabindex: Only the selected tab should be in the tab order.

FAQ

Do I need role='tabpanel' on the content panels?

Yes. role='tabpanel' is required for the panels.

Should tabs be

Use

Can tabs be vertical?

Yes. For vertical tabs, use Up/Down arrows instead of Left/Right.

Do I need to manage focus when a tab is clicked?

Clicking a tab activates it. Arrow keys also activate the new tab.

What happens if a user tabs into the tabpanel?

Tab moves focus into the active tabpanel content. Shift+Tab moves back to the tabs.

Mini Project

You just built the mini project. Add two more features: support for vertical tabs and dynamic tab addition/removal.

What's Next

This completes the ARIA Guide series. Continue to Keyboard Navigation for accessible keyboard interactions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro