Chapter 6: Tabs
How to create tabs (also called tabbed interfaces, tab navigation, tab panels, etc.) from the very beginning.
What are Tabs?
Tabs are a very common UI pattern that lets users switch between different sections of content without leaving the page.
You see them everywhere:
- Product pages (Description / Reviews / Specifications)
- Settings pages
- Dashboard analytics
- FAQ pages
- Code editors (multiple files open)
- Browser dev tools
Goals of good tabs (keep this in mind)
- Clear which tab is active
- Easy to click / tap (especially on mobile)
- Content changes instantly or with smooth transition
- Accessible with keyboard (Tab key + Enter/Space)
- Works well on small screens
- Only loads/shows relevant content
We will build four progressively better versions:
- Pure CSS tabs (no JavaScript) – very simple
- Classic JavaScript tabs (most common style)
- Modern accessible tabs with <tablist>, <tab>, <tabpanel> roles
- Bonus: Tabs with fade / slide animation
Let’s go slowly.
Version 1 – Pure CSS Tabs (no JavaScript)
This uses radio buttons + labels (very clever trick).
HTML
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<div class="tabs-container"> <!-- Hidden radio buttons control which tab is active --> <input type="radio" name="tabs" id="tab1" checked> <input type="radio" name="tabs" id="tab2"> <input type="radio" name="tabs" id="tab3"> <!-- Tab navigation bar --> <div class="tab-list"> <label for="tab1" class="tab-button">Profile</label> <label for="tab2" class="tab-button">Settings</label> <label for="tab3" class="tab-button">Security</label> </div> <!-- Tab contents --> <div class="tab-content"> <div class="tab-panel" id="content1"> <h2>Your Profile</h2> <p>Here you can update your name, photo, bio, etc.</p> </div> <div class="tab-panel" id="content2"> <h2>Account Settings</h2> <p>Language, notifications, dark mode, email preferences...</p> </div> <div class="tab-panel" id="content3"> <h2>Security & Privacy</h2> <p>Change password, two-factor authentication, login history.</p> </div> </div> </div> |
CSS
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
.tabs-container { max-width: 780px; margin: 2rem auto; font-family: system-ui, sans-serif; } .tab-list { display: flex; border-bottom: 2px solid #e0e0e0; } .tab-button { flex: 1; padding: 1rem 1.5rem; text-align: center; background: #f8f9fa; cursor: pointer; font-weight: 500; transition: all 0.2s; user-select: none; } .tab-button:hover { background: #e9ecef; } /* Active tab styling */ #tab1:checked ~ .tab-list label[for="tab1"], #tab2:checked ~ .tab-list label[for="tab2"], #tab3:checked ~ .tab-list label[for="tab3"] { background: white; border-bottom: 3px solid #0066ff; color: #0066ff; font-weight: 600; } /* Show only the selected panel */ .tab-panel { display: none; padding: 1.8rem 2rem; background: white; border: 1px solid #e0e0e0; border-top: none; } #tab1:checked ~ .tab-content #content1, #tab2:checked ~ .tab-content #content2, #tab3:checked ~ .tab-content #content3 { display: block; } |
Pros
- No JavaScript
- Very fast
- Works with JS disabled
Cons
- Only one tab active at a time (by design)
- Harder to add dynamic content later
- Accessibility is okay but not great
Version 2 – JavaScript Tabs (most common real-world approach)
HTML
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<div class="tabs"> <div class="tab-buttons"> <button class="tab-btn active" data-tab="profile">Profile</button> <button class="tab-btn" data-tab="settings">Settings</button> <button class="tab-btn" data-tab="security">Security</button> </div> <div class="tab-panels"> <div class="tab-panel active" id="profile"> <h2>Profile</h2> <p>Update your personal information here.</p> </div> <div class="tab-panel" id="settings"> <h2>Settings</h2> <p>Customize your experience.</p> </div> <div class="tab-panel" id="security"> <h2>Security</h2> <p>Protect your account.</p> </div> </div> </div> |
CSS
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
.tab-buttons { display: flex; border-bottom: 2px solid #ddd; } .tab-btn { padding: 0.9rem 1.8rem; background: none; border: none; font-size: 1.05rem; cursor: pointer; color: #555; transition: all 0.2s; } .tab-btn:hover { background: #f5f5f5; } .tab-btn.active { border-bottom: 3px solid #0077ff; color: #0077ff; font-weight: 600; } .tab-panels { position: relative; } .tab-panel { display: none; padding: 2rem; background: white; border: 1px solid #ddd; border-top: none; } .tab-panel.active { display: block; } |
JavaScript
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
document.addEventListener("DOMContentLoaded", () => { const tabs = document.querySelectorAll(".tab-btn"); tabs.forEach(tab => { tab.addEventListener("click", () => { // Remove active from all buttons & panels tabs.forEach(t => t.classList.remove("active")); document.querySelectorAll(".tab-panel").forEach(p => p.classList.remove("active")); // Add active to clicked tab tab.classList.add("active"); // Show corresponding panel const target = tab.getAttribute("data-tab"); document.getElementById(target).classList.add("active"); }); }); }); |
Very important: data-tab attribute links button to panel id.
Version 3 – Accessible Tabs (ARIA + best practices 2025–2026)
HTML
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<div class="tabs"> <div role="tablist" class="tab-buttons"> <button role="tab" aria-selected="true" aria-controls="profile-panel" id="profile-tab" class="tab-btn active"> Profile </button> <button role="tab" aria-selected="false" aria-controls="settings-panel" id="settings-tab" class="tab-btn"> Settings </button> <button role="tab" aria-selected="false" aria-controls="security-panel" id="security-tab" class="tab-btn"> Security </button> </div> <div class="tab-panels"> <div role="tabpanel" id="profile-panel" tabindex="0" class="tab-panel active"> <h2>Profile</h2> <p>...</p> </div> <div role="tabpanel" id="settings-panel" tabindex="0" hidden class="tab-panel"> <h2>Settings</h2> <p>...</p> </div> <div role="tabpanel" id="security-panel" tabindex="0" hidden class="tab-panel"> <h2>Security</h2> <p>...</p> </div> </div> </div> |
JavaScript (with ARIA updates)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
document.querySelectorAll('[role="tab"]').forEach(tab => { tab.addEventListener("click", () => { // Deactivate all document.querySelectorAll('[role="tab"]').forEach(t => { t.setAttribute("aria-selected", "false"); t.classList.remove("active"); }); document.querySelectorAll('[role="tabpanel"]').forEach(p => { p.setAttribute("hidden", ""); p.classList.remove("active"); }); // Activate selected tab.setAttribute("aria-selected", "true"); tab.classList.add("active"); const panel = document.getElementById(tab.getAttribute("aria-controls")); panel.removeAttribute("hidden"); panel.classList.add("active"); }); // Keyboard support: Arrow keys to move between tabs tab.addEventListener("keydown", e => { if (e.key === "ArrowRight" || e.key === "ArrowLeft") { e.preventDefault(); const tabs = [...document.querySelectorAll('[role="tab"]')]; const current = tabs.indexOf(tab); let next; if (e.key === "ArrowRight") { next = tabs[(current + 1) % tabs.length]; } else { next = tabs[(current - 1 + tabs.length) % tabs.length]; } next.focus(); next.click(); } }); }); |
Quick comparison – which method to choose?
| Goal | Best choice | Needs JS? | Accessibility | Animation possible? | Mobile friendly |
|---|---|---|---|---|---|
| Super simple, no JS | Pure CSS (radio) | No | Medium | Limited | Good |
| Classic & flexible | JS + data attributes | Yes | Good | Yes | Very good |
| Professional / production | ARIA roles + keyboard support | Yes | Excellent | Yes | Excellent |
| Very modern look | Tailwind / Headless UI / Radix UI | Yes | Excellent | Yes | Perfect |
Bonus: Adding a nice fade transition
Add this CSS:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.tab-panel { opacity: 0; transition: opacity 0.25s ease; pointer-events: none; } .tab-panel.active { opacity: 1; pointer-events: auto; } |
Now panels fade in/out smoothly.
Would you like to go deeper into any of these topics?
Examples:
- How to make vertical tabs (left sidebar style)
- How to make tabs scroll horizontally on mobile
- How to lazy-load tab content (only load when opened)
- How to make tabs with icons
- How to do tabs in React / Vue / Tailwind
- Common accessibility mistakes
- How to animate with slide instead of fade
- How to make tabs look like browser tabs
Just tell me which part you want explained slowly with full examples — I’m here for you. 😊
