How to Build Toggle Tab Menus with CSS and JavaScript
Overview
A toggle tab menu lets users switch between panels/content by clicking tabs. Implementing one requires accessible markup, CSS for layout/visibility, and JavaScript to toggle active states and manage focus/ARIA for assistive tech.
HTML structure
- Use a tablist and buttons for tabs (keyboard focusable).
- Associate each tab with a panel via aria-controls and matching id. Example pattern:
html
<div role=“tablist” aria-label=“Sample Tabs”> <button role=“tab” aria-selected=“true” aria-controls=“panel-1” id=“tab-1”>Tab 1</button> <button role=“tab” aria-selected=“false” aria-controls=“panel-2” id=“tab-2”>Tab 2</button> <button role=“tab” aria-selected=“false” aria-controls=“panel-3” id=“tab-3”>Tab 3</button> </div> <div id=“panel-1” role=“tabpanel” aria-labelledby=“tab-1”>Content 1</div> <div id=“panel-2” role=“tabpanel” aria-labelledby=“tab-2” hidden>Content 2</div> <div id=“panel-3” role=“tabpanel” aria-labelledby=“tab-3” hidden>Content 3</div>
CSS
- Style active tab and hide inactive panels using [hidden] or a utility class.
- Ensure keyboard focus styles and responsive layout. Example essentials:
css
[role=“tab”] { cursor: pointer; padding: 8px 12px; border: none; background: none; } [role=“tab”][aria-selected=“true”] { border-bottom: 2px solid #007acc; font-weight: 600; } [role=“tabpanel”] { padding: 16px; } [role=“tabpanel”][hidden] { display: none; }
JavaScript behavior
- Attach click handlers to tabs to set aria-selected, update panels’ hidden state, and move focus if needed.
- Support keyboard navigation: Left/Right (or Up/Down) to switch tabs, Home/End to jump to first/last. Example minimal script:
js
const tabs = Array.from(document.querySelectorAll(’[role=“tab”]’)); const panels = Array.from(document.querySelectorAll(’[role=“tabpanel”]’)); function activateTab(tab) { tabs.forEach(t => t.setAttribute(‘aria-selected’,‘false’)); panels.forEach(p => p.hidden = true); tab.setAttribute(‘aria-selected’,‘true’); document.getElementById(tab.getAttribute(‘aria-controls’)).hidden = false; tab.focus(); } tabs.forEach(tab => { tab.addEventListener(‘click’, () => activateTab(tab)); tab.addEventListener(‘keydown’, (e) => { let idx = tabs.indexOf(tab); if (e.key === ‘ArrowRight’) idx = (idx + 1) % tabs.length; else if (e.key === ‘ArrowLeft’) idx = (idx - 1 + tabs.length) % tabs.length; else if (e.key === ‘Home’) idx = 0; else if (e.key === ‘End’) idx = tabs.length - 1; else return; e.preventDefault(); activateTab(tabs[idx]); }); });
Accessibility notes
- Use role=“tablist”, role=“tab”, role=“tabpanel”, aria-selected, aria-controls, aria-labelledby.
- Keep tabs as buttons (not links) so they are operable by keyboard.
- Manage focus and announce changes by ensuring panels are in DOM and shown/hidden with hidden, not removed.
Variations & enhancements
- Animate panel transitions with CSS (height/fade).
- Deep-linking: update URL hash on tab change and restore on load.
- Lazy-load panel content when first activated for performance.
- Make it responsive: convert to accordion pattern on small screens if needed.
Quick checklist
- Semantic HTML roles and attributes ✔
- Keyboard navigation implemented ✔
- Visible focus states ✔
- Inactive panels hidden (use hidden attribute) ✔
- Test with screen readers ✔
If you want, I can generate a complete ready-to-copy example (HTML/CSS/JS) tailored for responsive behavior or with animations.
Leave a Reply