ARIA Project — Build an Accessible Tab Panel
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.