Tabs
A Tab interface with minimal JavaScript.
Horizontal Tabs
Tab 1
This is the content for Tab 1.
Tab 2
This is the content for Tab 2.
Vertical Tabs
Vertical Tab 1
This is the content for Vertical Tab 1. Notice that the tabs are arranged vertically on the left side.
Vertical Tab 2
This is the content for Vertical Tab 2. Use up/down arrow keys for keyboard navigation with vertical tabs.
How It Works
This tab interface is built primarily using CSS with hidden radio inputs to control tab states. The core functionality (showing/hiding tab content and highlighting the active tab) works without any JavaScript through CSS selectors.
The implementation uses the CSS sibling selector ~
to
show/hide content and highlight the active tab, making it
lightweight and performant.
The JavaScript's role: A minimal amount of JavaScript is used solely for accessibility improvements:
-
ARIA attribute management: Updates
aria-selected
states dynamically when tabs change - Keyboard navigation: Enables arrow key navigation between tabs (adapting to orientation)
- Focus management: Moves focus to the newly activated tab when switching with keyboard
Without this JavaScript, the tabs would still function for mouse users, but would lack proper keyboard accessibility and screen reader announcements.
The component supports both horizontal and vertical orientations
using the aria-orientation
attribute and CSS classes.
Accessibility Features
ARIA Attributes Explained
- role="tablist": Identifies the container as a collection of tabs
- role="tab": Identifies each tab button/trigger
- role="tabpanel": Identifies each tab content panel
- aria-orientation="horizontal/vertical": Indicates the layout direction of tabs
- aria-selected="true/false": Indicates which tab is currently selected
- aria-controls="panel-id": Associates each tab with the panel it controls
- aria-labelledby="tab-id": Associates each panel with the tab that labels it
- tabindex="0": Makes tabs keyboard focusable
Keyboard Navigation
- Tab key: Moves focus to the tabs and between tabs and their panels
- Right/Down arrows: Move to next tab (depends on orientation)
- Left/Up arrows: Move to previous tab (depends on orientation)
- Enter/Space: Activates the tab that currently has keyboard focus
Focus Management
The minimal JavaScript enhances keyboard accessibility by:
- Supporting arrow key navigation between tabs (adapting to orientation)
- Automatically focusing the newly activated tab
- Ensuring proper ARIA states are updated when tabs change
What Not To Add
For best accessibility practices, avoid these common mistakes:
-
role="button" on tab elements: Not needed and
conflicts with
role="tab"
- aria-hidden="true" on inactive tab panels: The CSS hiding is sufficient, and using aria-hidden would remove them from the accessibility tree
- tabindex="-1" on inactive tab elements: All tabs should remain in the tab order for proper keyboard navigation
- Complex JavaScript: Adding unnecessary JavaScript when CSS can handle the core functionality
Additional Resources
Implementation Code
HTML
<!-- Tab container with radio inputs at the top level -->
<div class="tab-container">
<input
type="radio"
id="tab-1"
name="tab"
data-tab="tab-1"
checked
aria-controls="panel-1"
/>
<input
type="radio"
id="tab-2"
name="tab"
data-tab="tab-2"
aria-controls="panel-2"
/>
<!-- Tab header with tab buttons as labels -->
<div class="tab-header" role="tablist" aria-orientation="horizontal">
<label for="tab-1" class="tab-button" role="tab" tabindex="0" id="label-tab-1" aria-selected="true">Tab 1</label>
<label for="tab-2" class="tab-button" role="tab" tabindex="0" id="label-tab-2" aria-selected="false">Tab 2</label>
</div>
<!-- Tab content panels -->
<div class="tabs-content">
<div class="tab-content" id="panel-1" role="tabpanel" aria-labelledby="label-tab-1" tabindex="0" data-tab="tab-1">
<h2>Tab 1</h2>
<p>This is the content for Tab 1.</p>
</div>
<div class="tab-content" id="panel-2" role="tabpanel" aria-labelledby="label-tab-2" tabindex="0" data-tab="tab-2">
<h2>Tab 2</h2>
<p>This is the content for Tab 2.</p>
</div>
</div>
</div>
<!-- For vertical orientation, add the tabs-vertical class and change aria-orientation -->
<div class="tab-container tabs-vertical">
<!-- Radio inputs remain the same -->
<div class="tab-header" role="tablist" aria-orientation="vertical">
<!-- Tab labels remain the same -->
</div>
<div class="tabs-content">
<!-- Tab content panels remain the same -->
</div>
</div>
CSS
/* Hide radio buttons but keep them accessible */
input[type="radio"] {
display: none;
}
/* Style for tab buttons */
.tab-button {
background-color: var(--primary);
color: #fff;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
text-align: center;
}
/* Tab header layout - horizontal (default) */
.tab-header {
display: flex;
gap: 1rem;
padding: 1rem;
width: 100%;
}
/* Vertical orientation styles */
.tabs-vertical {
display: flex;
gap: 1rem;
}
.tabs-vertical .tab-header {
flex-direction: column;
width: 200px; /* Adjust as needed */
}
.tabs-vertical .tabs-content {
flex: 1;
}
/* Hide all tab content by default */
.tab-content {
display: none;
animation: fadeIn 0.3s ease-in-out;
}
/* Key CSS: Show content for active tab using sibling selector */
#tab-1:checked ~ .tabs-content .tab-content[data-tab="tab-1"],
#tab-2:checked ~ .tabs-content .tab-content[data-tab="tab-2"],
#v-tab-1:checked ~ .tabs-content .tab-content[data-tab="v-tab-1"],
#v-tab-2:checked ~ .tabs-content .tab-content[data-tab="v-tab-2"] {
display: block;
}
/* Key CSS: Highlight active tab label */
#tab-1:checked ~ .tab-header label[for="tab-1"],
#tab-2:checked ~ .tab-header label[for="tab-2"],
#v-tab-1:checked ~ .tab-header label[for="v-tab-1"],
#v-tab-2:checked ~ .tab-header label[for="v-tab-2"] {
background-color: var(--secondary, #0056b3);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
font-weight: bold;
}
/* Fade-in animation for tab content */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
JavaScript (for Accessibility)
// This script enhances keyboard accessibility and ARIA states
document.addEventListener('DOMContentLoaded', function() {
const tabButtons = document.querySelectorAll('.tab-button');
const tablists = document.querySelectorAll('[role="tablist"]');
// Add keyboard navigation with arrow keys
tablists.forEach(tablist => {
const isVertical = tablist.getAttribute('aria-orientation') === 'vertical';
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
tabs.forEach(tab => {
tab.addEventListener('keydown', function(e) {
// Determine which keys to use based on orientation
const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
// Handle arrow keys
if (e.key === nextKey || e.key === prevKey) {
e.preventDefault();
const currentIndex = tabs.indexOf(this);
let newIndex;
if (e.key === nextKey) {
newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
} else {
newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
}
// Activate new tab and move focus
tabs[newIndex].click();
tabs[newIndex].focus();
}
// Handle Enter or Space key
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
});
});
// Update ARIA attributes when tabs change
const radioButtons = document.querySelectorAll('input[type="radio"][name="tab"], input[type="radio"][name="v-tab"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
tabButtons.forEach(tab => {
const isSelected = tab.getAttribute('for') === this.id;
tab.setAttribute('aria-selected', isSelected ? 'true' : 'false');
});
});
});
});