UI Components Medium
ARIA Tabs Pattern
Fully accessible tab interface implementing WAI-ARIA tabs pattern with keyboard navigation and proper role attributes.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #e4e4e7;
line-height: 1.6;
min-height: 100vh;
}
.demo {
max-width: 720px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
.demo-title {
font-size: 1.75rem;
font-weight: 700;
color: #fafafa;
letter-spacing: -0.02em;
}
.demo-sub {
color: #a1a1aa;
margin-top: 0.25rem;
font-size: 0.95rem;
}
/* Tabs Wrapper */
.tabs-wrapper {
margin-top: 2rem;
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
overflow: hidden;
}
/* Tablist */
.tablist {
display: flex;
border-bottom: 1px solid #27272a;
background: #0e0e10;
overflow-x: auto;
}
.tab {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.85rem 1.25rem;
font-size: 0.85rem;
font-weight: 500;
font-family: inherit;
color: #71717a;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.tab:hover {
color: #d4d4d8;
background: rgba(255, 255, 255, 0.03);
}
.tab--active {
color: #fafafa;
border-bottom-color: #3b82f6;
}
.tab:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: -2px;
border-radius: 4px;
}
.tab-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* Tab Panels */
.tabpanel {
padding: 1.5rem;
display: none;
animation: fadeIn 0.2s ease-out;
}
.tabpanel--active,
.tabpanel:not([hidden]) {
display: block;
}
.tabpanel:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
.panel-heading {
font-size: 1.1rem;
font-weight: 600;
color: #fafafa;
margin-bottom: 1.25rem;
}
/* Form */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: #a1a1aa;
margin-bottom: 0.35rem;
}
.form-input {
width: 100%;
padding: 0.6rem 0.85rem;
background: #09090b;
border: 1px solid #27272a;
border-radius: 8px;
color: #e4e4e7;
font-size: 0.875rem;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.radio-group {
display: flex;
gap: 1.25rem;
}
.radio-label {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.875rem;
color: #d4d4d8;
cursor: pointer;
}
/* Members */
.member-list {
list-style: none;
}
.member-item {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.75rem 0;
}
.member-item + .member-item {
border-top: 1px solid #1e1e22;
}
.member-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: #fff;
flex-shrink: 0;
}
.member-info {
display: flex;
flex-direction: column;
}
.member-name {
font-size: 0.875rem;
font-weight: 500;
color: #fafafa;
}
.member-role {
font-size: 0.75rem;
color: #71717a;
}
/* Toggles */
.toggle-list {
display: flex;
flex-direction: column;
}
.toggle-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.85rem 0;
cursor: pointer;
}
.toggle-item + .toggle-item {
border-top: 1px solid #1e1e22;
}
.toggle-text {
display: flex;
flex-direction: column;
}
.toggle-text strong {
font-size: 0.875rem;
font-weight: 500;
color: #e4e4e7;
}
.toggle-desc {
font-size: 0.78rem;
color: #71717a;
margin-top: 0.1rem;
}
.toggle-input {
width: 40px;
height: 22px;
appearance: none;
background: #27272a;
border-radius: 100px;
position: relative;
cursor: pointer;
flex-shrink: 0;
transition: background 0.2s;
}
.toggle-input::before {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #71717a;
transition: transform 0.2s, background 0.2s;
}
.toggle-input:checked {
background: #3b82f6;
}
.toggle-input:checked::before {
transform: translateX(18px);
background: #fff;
}
.toggle-input:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Billing */
.billing-card {
background: #09090b;
border: 1px solid #27272a;
border-radius: 10px;
padding: 1.25rem;
}
.billing-plan {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.billing-plan-name {
font-size: 1rem;
font-weight: 600;
color: #3b82f6;
}
.billing-plan-price {
font-size: 1.5rem;
font-weight: 700;
color: #fafafa;
}
.billing-plan-period {
font-size: 0.8rem;
font-weight: 400;
color: #71717a;
}
.billing-plan-desc {
font-size: 0.85rem;
color: #a1a1aa;
margin-bottom: 1rem;
}
.billing-meta {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.8rem;
color: #71717a;
}
.billing-meta strong {
color: #d4d4d8;
}
/* Info Panel */
.info-panel {
margin-top: 2rem;
background: #111113;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.5rem;
}
.info-title {
font-size: 1rem;
font-weight: 600;
color: #fafafa;
margin-bottom: 0.75rem;
}
.info-subtitle {
font-size: 0.9rem;
font-weight: 600;
color: #fafafa;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
.info-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.info-list li {
font-size: 0.825rem;
color: #a1a1aa;
}
.info-list code {
background: #1e1e22;
padding: 0.12rem 0.4rem;
border-radius: 4px;
font-size: 0.78rem;
color: #60a5fa;
font-family: "SF Mono", "Fira Code", monospace;
}
.kbd-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.kbd-item {
font-size: 0.825rem;
color: #a1a1aa;
}
kbd {
display: inline-block;
padding: 0.1rem 0.45rem;
background: #1e1e22;
border: 1px solid #3f3f46;
border-radius: 4px;
font-size: 0.75rem;
font-family: "SF Mono", "Fira Code", monospace;
color: #d4d4d8;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}(() => {
const tablist = document.querySelector('[role="tablist"]');
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const panels = tabs.map((tab) => document.getElementById(tab.getAttribute("aria-controls")));
function activateTab(tab) {
// Deactivate all tabs
tabs.forEach((t) => {
t.setAttribute("aria-selected", "false");
t.setAttribute("tabindex", "-1");
t.classList.remove("tab--active");
});
// Hide all panels
panels.forEach((p) => {
p.hidden = true;
p.classList.remove("tabpanel--active");
});
// Activate selected tab
tab.setAttribute("aria-selected", "true");
tab.setAttribute("tabindex", "0");
tab.classList.add("tab--active");
tab.focus();
// Show associated panel
const panel = document.getElementById(tab.getAttribute("aria-controls"));
panel.hidden = false;
panel.classList.add("tabpanel--active");
}
// Click handler
tabs.forEach((tab) => {
tab.addEventListener("click", () => activateTab(tab));
});
// Keyboard handler
tablist.addEventListener("keydown", (e) => {
const currentIndex = tabs.indexOf(document.activeElement);
if (currentIndex === -1) return;
let newIndex;
switch (e.key) {
case "ArrowRight":
e.preventDefault();
newIndex = (currentIndex + 1) % tabs.length;
activateTab(tabs[newIndex]);
break;
case "ArrowLeft":
e.preventDefault();
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
activateTab(tabs[newIndex]);
break;
case "Home":
e.preventDefault();
activateTab(tabs[0]);
break;
case "End":
e.preventDefault();
activateTab(tabs[tabs.length - 1]);
break;
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ARIA Tabs Pattern</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">ARIA Tabs Pattern</h1>
<p class="demo-sub">WAI-ARIA compliant tabs with keyboard navigation and proper role attributes.</p>
<div class="tabs-wrapper">
<div role="tablist" aria-label="Project settings" class="tablist">
<button role="tab"
id="tab-general"
aria-selected="true"
aria-controls="panel-general"
tabindex="0"
class="tab tab--active">
<svg class="tab-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
General
</button>
<button role="tab"
id="tab-members"
aria-selected="false"
aria-controls="panel-members"
tabindex="-1"
class="tab">
<svg class="tab-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Members
</button>
<button role="tab"
id="tab-notifications"
aria-selected="false"
aria-controls="panel-notifications"
tabindex="-1"
class="tab">
<svg class="tab-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
Notifications
</button>
<button role="tab"
id="tab-billing"
aria-selected="false"
aria-controls="panel-billing"
tabindex="-1"
class="tab">
<svg class="tab-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
Billing
</button>
</div>
<!-- General Panel -->
<div role="tabpanel"
id="panel-general"
aria-labelledby="tab-general"
tabindex="0"
class="tabpanel tabpanel--active">
<h3 class="panel-heading">General Settings</h3>
<div class="form-group">
<label class="form-label" for="project-name">Project Name</label>
<input type="text" id="project-name" class="form-input" value="ARIA Components" />
</div>
<div class="form-group">
<label class="form-label" for="project-desc">Description</label>
<textarea id="project-desc" class="form-input form-textarea" rows="3">A collection of accessible UI components implementing WAI-ARIA design patterns.</textarea>
</div>
<div class="form-group">
<label class="form-label">Visibility</label>
<div class="radio-group">
<label class="radio-label"><input type="radio" name="visibility" checked /> Public</label>
<label class="radio-label"><input type="radio" name="visibility" /> Private</label>
</div>
</div>
</div>
<!-- Members Panel -->
<div role="tabpanel"
id="panel-members"
aria-labelledby="tab-members"
tabindex="0"
class="tabpanel"
hidden>
<h3 class="panel-heading">Team Members</h3>
<ul class="member-list">
<li class="member-item">
<div class="member-avatar" style="background:#3b82f6;">AL</div>
<div class="member-info"><span class="member-name">Alice Lin</span><span class="member-role">Owner</span></div>
</li>
<li class="member-item">
<div class="member-avatar" style="background:#8b5cf6;">BK</div>
<div class="member-info"><span class="member-name">Bob Kim</span><span class="member-role">Admin</span></div>
</li>
<li class="member-item">
<div class="member-avatar" style="background:#ec4899;">CR</div>
<div class="member-info"><span class="member-name">Carol Rivera</span><span class="member-role">Member</span></div>
</li>
<li class="member-item">
<div class="member-avatar" style="background:#f59e0b;">DT</div>
<div class="member-info"><span class="member-name">Dan Torres</span><span class="member-role">Member</span></div>
</li>
</ul>
</div>
<!-- Notifications Panel -->
<div role="tabpanel"
id="panel-notifications"
aria-labelledby="tab-notifications"
tabindex="0"
class="tabpanel"
hidden>
<h3 class="panel-heading">Notification Preferences</h3>
<div class="toggle-list">
<label class="toggle-item">
<span class="toggle-text">
<strong>Email notifications</strong>
<span class="toggle-desc">Receive email for important updates</span>
</span>
<input type="checkbox" class="toggle-input" checked />
</label>
<label class="toggle-item">
<span class="toggle-text">
<strong>Push notifications</strong>
<span class="toggle-desc">Browser push notifications for real-time alerts</span>
</span>
<input type="checkbox" class="toggle-input" />
</label>
<label class="toggle-item">
<span class="toggle-text">
<strong>Weekly digest</strong>
<span class="toggle-desc">Summary email sent every Monday</span>
</span>
<input type="checkbox" class="toggle-input" checked />
</label>
<label class="toggle-item">
<span class="toggle-text">
<strong>Marketing emails</strong>
<span class="toggle-desc">Product updates and feature announcements</span>
</span>
<input type="checkbox" class="toggle-input" />
</label>
</div>
</div>
<!-- Billing Panel -->
<div role="tabpanel"
id="panel-billing"
aria-labelledby="tab-billing"
tabindex="0"
class="tabpanel"
hidden>
<h3 class="panel-heading">Billing Information</h3>
<div class="billing-card">
<div class="billing-plan">
<span class="billing-plan-name">Pro Plan</span>
<span class="billing-plan-price">$29<span class="billing-plan-period">/month</span></span>
</div>
<p class="billing-plan-desc">Unlimited projects, priority support, advanced analytics.</p>
<div class="billing-meta">
<span>Next billing date: <strong>April 1, 2026</strong></span>
<span>Payment method: <strong>Visa ending in 4242</strong></span>
</div>
</div>
</div>
</div>
<!-- ARIA Info Panel -->
<div class="info-panel">
<h3 class="info-title">ARIA Attributes Used</h3>
<ul class="info-list">
<li><code>role="tablist"</code> — Container for the tab buttons</li>
<li><code>role="tab"</code> — Each clickable tab button</li>
<li><code>role="tabpanel"</code> — Content panel associated with a tab</li>
<li><code>aria-selected="true/false"</code> — Indicates the active tab</li>
<li><code>aria-controls</code> — Links a tab to its panel</li>
<li><code>aria-labelledby</code> — Links a panel back to its tab</li>
<li><code>tabindex="0/-1"</code> — Roving tabindex for keyboard focus</li>
</ul>
<h4 class="info-subtitle">Keyboard Controls</h4>
<div class="kbd-list">
<span class="kbd-item"><kbd>Left</kbd> / <kbd>Right</kbd> — Move between tabs</span>
<span class="kbd-item"><kbd>Home</kbd> — Go to first tab</span>
<span class="kbd-item"><kbd>End</kbd> — Go to last tab</span>
<span class="kbd-item"><kbd>Tab</kbd> — Enter/leave the tab panel</span>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>A tabbed interface implementing the WAI-ARIA Tabs design pattern with full keyboard navigation (Left/Right arrows, Home/End) and automatic activation. All tabs use proper role, aria-selected, aria-controls, and aria-labelledby attributes.