Live Demonstration
Interact with the tabs below—you can also use arrow keys to navigate them. The core tab switching logic is now handled by JavaScript, allowing for a more robust and extensible component.
Overview of all active projects. Use the search below to filter projects.
Details for Sub-Project Alpha, including its specific tasks and resource allocation.
Details for Sub-Project Beta, focusing on its timeline and deliverables.
Pending Tasks
- Due: Tomorrow
- Due: Oct 28
- Due: Nov 2
Project Timelines
Alex Vance
Lead Developer
Brianna Shaw
UX/UI Designer
Carlos Jimenez
Project Manager
How It Works
This component has been engineered with a JavaScript-driven approach to create a modern, accessible, and extensible tab system. Unlike older methods that rely on radio-button hacks, our approach uses semantic HTML and is designed to work within modern CSS layouts like Flexbox or Grid. This model offers significant advantages:
- •Superior Accessibility: Implements full keyboard navigation (arrow keys, Home/End) and ARIA roles for screen reader compatibility.
- •Semantic & Modern Structure: Uses proper
<button>
and<div>
elements, designed for modern CSS layouts like Grid. This avoids legacy radio button hacks for cleaner, more maintainable code. - •Future-Ready: The structure is easily expandable with features like lazy-loading content.
The script manages the component's state by toggling an .active
class for styling, while also updating key ARIA attributes (like aria-selected
) and the tabindex
to ensure the component is fully accessible. CSS handles all visual styling, and the only trade-off is that JavaScript is required, which is a standard for modern web applications.
Want to see the code in action? You can review the example below or open the full interactive editor to modify the code and see your changes instantly.
Technical Documentation: ARIA & Accessibility
This tab component is not just about looks; it's built from the ground up with accessibility as a top priority. We intentionally use semantic HTML and avoid obsolete "radio button hacks," allowing the component to integrate cleanly into modern CSS layouts like Grid or Flexbox. This modern foundation allows us to fully adhere to the WAI-ARIA Authoring Practices Guide (APG) for Tabs to ensure the component is robust, understandable, and fully operable for all users, including those who rely on assistive technologies like screen readers or keyboard-only navigation.
The implementation correctly utilizes the following ARIA roles and properties to create a fully accessible tabbed interface:
Core Roles & Relationships
-
role="tablist"
Applied to the button container, this role defines the element as a list of tabs, allowing screen readers to announce it as a single, cohesive widget.
-
role="tab"
Identifies each button as an individual tab control. This role is essential for assistive technology to understand its function.
-
role="tabpanel"
Identifies each content area that is associated with a tab, clearly defining its purpose as a panel of information.
-
aria-controls
&aria-labelledby
These properties create an explicit, two-way link:
aria-controls
on a tab points to its panel's ID, whilearia-labelledby
on a panel points to its controlling tab's ID. This robust connection is crucial for assistive technologies to announce the relationship between the active tab and its visible content panel.
State Management & Keyboard Interaction
-
aria-selected="true | false"
Dynamically updated by the script, this state tells assistive technologies exactly which tab is currently active. Announcing "Tab, [Name], selected" provides clear feedback to the user.
-
Roving
tabindex
To create an intuitive keyboard experience, only the active tab has
tabindex="0"
. All other tabs are set totabindex="-1"
, removing them from the standard tab sequence. This prevents users from having to tediously tab through every single tab label. -
Full Keyboard Support
Once a tab has focus, keyboard navigation is as follows:
• Right/Left Arrows: Move between tabs. Navigation stops at the first and last tabs, allowing the event to bubble up to a parent tab component if one exists (solving the "focus trap" problem with nested tabs).
• Home: Jump to the first tab in the list.
• End: Jump to the last tab in the list.
• Up/Down Arrows: Intentionally not handled to allow for standard page scrolling.
Quick Start: Ready-to-Use Code
To integrate these tabs into your project, copy the following HTML, CSS, and JavaScript. This provides a minimal, accessible, and functional starting point.
1. HTML Structure
This is the semantic foundation. The .tabs-container
is the component's root. Note the use of ARIA roles and the crucial link between buttons (via aria-controls
) and content panels (via id
).
<!-- The main container for the tab component -->
<div class="tabs-container">
<!-- The container for tab buttons (labels) -->
<div class="tab-group" role="tablist" aria-label="Example Tab List">
<button id="label-for-panel-1" class="tab-label-base active" role="tab" aria-controls="panel-1" aria-selected="true" tabindex="0">Tab One</button>
<button id="label-for-panel-2" class="tab-label-base" role="tab" aria-controls="panel-2" aria-selected="false" tabindex="-1">Tab Two</button>
</div>
<!-- The container for the tab content panels -->
<div class="tab-content-container">
<div id="panel-1" class="tab-content active" role="tabpanel" aria-labelledby="label-for-panel-1">
<p>Content for the first tab.</p>
</div>
<div id="panel-2" class="tab-content" role="tabpanel" aria-labelledby="label-for-panel-2">
<p>Content for the second tab.</p>
</div>
</div>
</div>
2. Core CSS
This CSS provides the essential styling for visibility, layout, and a clean default appearance. It's built with modern features like :focus-visible
for accessibility and an animation for a smooth user experience.
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.tab-group {
display: flex;
overflow-x: auto;
border-bottom: 2px solid #d1d5db; /* gray-300 */
}
.tab-label-base {
cursor: pointer;
padding: 0.75rem 1.25rem;
font-weight: 500;
border: 2px solid transparent;
border-bottom: none;
background-color: transparent;
white-space: nowrap;
position: relative;
bottom: -2px; /* Sits on top of the container's border */
transition: all 0.2s ease-in-out;
}
.tab-label-base:hover {
background-color: #f3f4f6; /* gray-100 */
}
.tab-label-base:focus-visible {
outline: 2px solid #3b82f6; /* blue-500 */
outline-offset: -2px;
border-radius: 0.25rem;
}
.tab-label-base.active {
font-weight: 600;
color: #1e40af; /* blue-800 */
border-color: #d1d5db;
border-radius: 0.5rem 0.5rem 0 0;
background-color: #ffffff;
}
.tab-content {
display: none; /* Hide inactive panels */
animation: fadeIn 0.3s ease-out;
padding: 1.5rem;
border: 2px solid #d1d5db;
border-top: none;
border-radius: 0 0 0.5rem 0.5rem;
}
.tab-content.active {
display: block; /* Show the active panel */
}
3. JavaScript Logic
This script brings the component to life. It handles clicks, keyboard navigation, and updates all necessary ARIA attributes for full accessibility. It's self-contained and manages its own state.
document.addEventListener('DOMContentLoaded', () => {
/**
* Initializes a single tab component.
* @param {HTMLElement} tabContainer - The .tabs-container element.
*/
const setupTabComponent = (tabContainer) => {
const tablist = tabContainer.querySelector('[role="tablist"]');
if (!tablist) return;
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const switchTab = (newTab) => {
if (!newTab || newTab.getAttribute('aria-selected') === 'true') {
return; // Do nothing if already active
}
const currentActiveTab = tablist.querySelector('[aria-selected="true"]');
if (currentActiveTab) {
currentActiveTab.classList.remove('active');
currentActiveTab.setAttribute('aria-selected', 'false');
currentActiveTab.setAttribute('tabindex', '-1');
const currentPanelId = currentActiveTab.getAttribute('aria-controls');
// By scoping the query to the tabContainer, we ensure component encapsulation.
// This prevents conflicts if multiple tab components on the same page
// were to accidentally use the same ID for a panel.
const currentPanel = tabContainer.querySelector(`#${currentPanelId}`);
if (currentPanel) {
currentPanel.classList.remove('active');
}
}
newTab.classList.add('active');
newTab.setAttribute('aria-selected', 'true');
newTab.setAttribute('tabindex', '0');
const newPanelId = newTab.getAttribute('aria-controls');
// Scoping this query is crucial for robust, reusable components.
const newPanel = tabContainer.querySelector(`#${newPanelId}`);
if (newPanel) {
newPanel.classList.add('active');
}
newTab.focus({ preventScroll: true });
};
// Use event delegation for click handling.
tablist.addEventListener('click', (e) => {
const tab = e.target.closest('[role="tab"]');
// Ensure the clicked element is a tab and belongs to this specific tablist.
if (tab && tab.closest('[role="tablist"]') === tablist) {
switchTab(tab);
}
});
// Add keyboard navigation to the tab list.
tablist.addEventListener('keydown', (e) => {
const tab = e.target.closest('[role="tab"]');
// Ensure the event is from a tab within this component
if (!tab || tab.closest('[role="tablist"]') !== tablist) {
return;
}
const currentIndex = tabs.indexOf(tab);
if (currentIndex === -1) return;
let nextIndex = -1;
if (e.key === 'ArrowRight') {
e.preventDefault();
// If not the last tab, move to the next one.
// Otherwise, do nothing, allowing the event to bubble up to a parent tablist.
if (currentIndex < tabs.length - 1) {
nextIndex = currentIndex + 1;
}
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
// If not the first tab, move to the previous one.
// Otherwise, do nothing, allowing the event to bubble up.
if (currentIndex > 0) {
nextIndex = currentIndex - 1;
}
} else if (e.key === 'Home') {
e.preventDefault();
nextIndex = 0;
} else if (e.key === 'End') {
e.preventDefault();
nextIndex = tabs.length - 1;
}
if (nextIndex !== -1) {
// Stop propagation only if we are handling the navigation internally.
// This allows arrow keys to "escape" a nested tab component.
e.stopPropagation();
switchTab(tabs[nextIndex]);
}
});
};
// Initialize all tab components on the page.
document.querySelectorAll('.tabs-container').forEach(setupTabComponent);
});
In conclusion, this implementation does not just simulate the appearance of tabs; it correctly implements the full ARIA design pattern, resulting in a component that is both functional and universally accessible.