Pure HTML, CSS & JS Tabs No More Radio Button Hacks.

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

Phase 1: Research & Discovery 100%
Phase 2: UI/UX Design 75%
Phase 3: Development 40%
AV

Alex Vance

Lead Developer

BS

Brianna Shaw

UX/UI Designer

CJ

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.

Example Code (HTML, CSS, JS)

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, while aria-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 to tabindex="-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).

HTML
<!-- 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.

CSS
@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.

JavaScript
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.

Live Code Editor & Preview

HTML, CSS & JS Code

Live Preview