tab
A grouping label providing a mechanism for selecting the tab content that is to be rendered to the user. Part of a tablist, controlling a tabpanel.
Overview
The tab role indicates an interactive element that, when activated, displays its associated tabpanel. Tabs are always part of a tablist and work together to provide a tabbed interface pattern.
Only one tab in a tablist should be active at a time, indicated by aria-selected="true". All other tabs should have aria-selected="false".
Live Demo: Tab Interface
Keyboard navigation: Use Left/Right Arrow to switch tabs, Home/End to go to first/last tab.
Code Examples
Basic Tab Pattern
<!-- Basic Tab Pattern -->
<div class="tabs">
<!-- Tab List -->
<div role="tablist" aria-label="Content sections">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
tabindex="0"
>
Tab 1
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1"
>
Tab 2
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-3"
id="tab-3"
tabindex="-1"
>
Tab 3
</button>
</div>
<!-- Tab Panels -->
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
Content for tab 1
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden>
Content for tab 2
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden>
Content for tab 3
</div>
</div>Full Keyboard Support
<!-- Tab with Full Keyboard Support -->
<script>
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');
tabs.forEach((tab, index) => {
tab.addEventListener('click', () => selectTab(index));
tab.addEventListener('keydown', (e) => {
let newIndex = index;
if (e.key === 'ArrowRight') {
e.preventDefault();
newIndex = (index + 1) % tabs.length;
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
newIndex = (index - 1 + tabs.length) % tabs.length;
} else if (e.key === 'Home') {
e.preventDefault();
newIndex = 0;
} else if (e.key === 'End') {
e.preventDefault();
newIndex = tabs.length - 1;
} else {
return; // Don't handle other keys
}
selectTab(newIndex);
tabs[newIndex].focus();
});
});
function selectTab(index) {
// Update tabs
tabs.forEach((tab, i) => {
const isSelected = i === index;
tab.setAttribute('aria-selected', isSelected);
tab.setAttribute('tabindex', isSelected ? '0' : '-1');
});
// Update panels
panels.forEach((panel, i) => {
panel.hidden = i !== index;
});
}
</script>React Component
// React Tabs Component
import { useState } from 'react';
interface Tab {
id: string;
label: string;
content: React.ReactNode;
}
function Tabs({ tabs }: { tabs: Tab[] }) {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
let newIndex = index;
if (e.key === 'ArrowRight') {
e.preventDefault();
newIndex = (index + 1) % tabs.length;
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
newIndex = (index - 1 + tabs.length) % tabs.length;
} else if (e.key === 'Home') {
e.preventDefault();
newIndex = 0;
} else if (e.key === 'End') {
e.preventDefault();
newIndex = tabs.length - 1;
} else {
return;
}
setActiveIndex(newIndex);
};
return (
<div className="tabs">
{/* Tab List */}
<div role="tablist" aria-label="Content sections" className="flex border-b">
{tabs.map((tab, index) => (
<button
key={tab.id}
role="tab"
aria-selected={activeIndex === index}
aria-controls={`panel-${tab.id}`}
id={`tab-${tab.id}`}
tabIndex={activeIndex === index ? 0 : -1}
onClick={() => setActiveIndex(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
className={`px-4 py-2 ${
activeIndex === index
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-600'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab Panels */}
{tabs.map((tab, index) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeIndex !== index}
tabIndex={0}
className="p-4"
>
{tab.content}
</div>
))}
</div>
);
}
// Usage
function App() {
const tabs = [
{ id: 'home', label: 'Home', content: <div>Home content</div> },
{ id: 'profile', label: 'Profile', content: <div>Profile content</div> },
{ id: 'settings', label: 'Settings', content: <div>Settings content</div> },
];
return <Tabs tabs={tabs} />;
}Keyboard Support
Best Practices
Only one tab should have aria-selected="true" at a time
Inactive tabs should have tabindex="-1"
Tab labels should be concise and descriptive
Use aria-controls to link tabs to their tabpanels
Provide clear visual indication of the selected tab
Don't use tabs for navigation between pages - use links instead
Don't hide inactive tabpanels with CSS only - use the hidden attribute
Don't forget to implement arrow key navigation

