Mobile Navigation Menu
Create accessible mobile navigation with hamburger menus. Ensure touch targets are large enough and expandable sections are properly announced.
The Bad (Inaccessible)
<div class="mobile-menu" onclick="toggleMenu()">
<div class="hamburger"></div>
</div>
<div class="menu-items" style="display:none">
<a href="/">Home</a>
<a href="/about">About</a>
</div>
Accessibility-Ready Code
<!-- Gold Standard: Button with ARIA states -->
<button
type="button"
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Open navigation menu"
class="mobile-toggle"
>
<span class="hamburger" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</span>
</button>
<!-- Collapsible menu panel -->
<div id="mobile-menu" class="menu-panel" hidden>
<nav aria-label="Mobile navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<script>
const toggle = document.querySelector('.mobile-toggle');
const menu = document.getElementById('mobile-menu');
toggle.addEventListener('click', () => {
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', !isExpanded);
toggle.setAttribute('aria-label', isExpanded ? 'Open navigation menu' : 'Close navigation menu');
menu.hidden = isExpanded;
});
</script>
// Gold Standard: Button with ARIA states
button(
type="button"
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Open navigation menu"
class="mobile-toggle"
)
span.hamburger(aria-hidden="true")
span
span
span
// Collapsible menu panel
div#mobile-menu.menu-panel(hidden)
nav(aria-label="Mobile navigation")
ul
li
a(href="/") Home
li
a(href="/about") About
li
a(href="/services") Services
li
a(href="/contact") Contact
const MobileNav = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
type="button"
aria-expanded={isOpen}
aria-controls="mobile-menu"
aria-label={isOpen ? 'Close navigation menu' : 'Open navigation menu'}
onClick={() => setIsOpen(!isOpen)}
className="mobile-toggle"
>
<span className="hamburger" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</span>
</button>
<div id="mobile-menu" className="menu-panel" hidden={!isOpen}>
<nav aria-label="Mobile navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
</>
);
};
<template>
<button
type="button"
:aria-expanded="isOpen"
aria-controls="mobile-menu"
:aria-label="isOpen ? 'Close navigation menu' : 'Open navigation menu'"
@click="isOpen = !isOpen"
class="mobile-toggle"
>
<span class="hamburger" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</span>
</button>
<div id="mobile-menu" class="menu-panel" v-show="isOpen">
<nav aria-label="Mobile navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
</template>
<script>
export default {
data() {
return { isOpen: false }
}
}
</script>
<button
type="button"
:aria-expanded="isOpen"
aria-controls="mobile-menu"
:aria-label="isOpen ? 'Close navigation menu' : 'Open navigation menu'"
@click="isOpen = !isOpen"
className="min-w-[44px] min-h-[44px] p-4"
>
<span aria-hidden="true" className="block space-y-1">
<span className="block w-6 h-0.5 bg-current"></span>
<span className="block w-6 h-0.5 bg-current"></span>
<span className="block w-6 h-0.5 bg-current"></span>
</span>
</button>
<div id="mobile-menu" className="bg-white shadow-lg" v-show="isOpen">
<nav aria-label="Mobile navigation">
<ul className="p-4 space-y-3">
<li><a href="/" className="block py-2">Home</a></li>
<li><a href="/about" className="block py-2">About</a></li>
<li><a href="/services" className="block py-2">Services</a></li>
<li><a href="/contact" className="block py-2">Contact</a></li>
</ul>
</nav>
</div>
The Standard
Mobile navigation must be fully accessible to keyboard and screen reader users. The key is using proper button elements with ARIA states that update dynamically.
WCAG Criteria
- 2.4.5 Multiple Ways: Provide multiple ways to access content.
- 2.5.5 Target Size: Touch targets at least 44×44px.
- 3.2.1 On Focus: No context changes on focus.
❌ The Bad (Inaccessible)
What’s Wrong?
- Using
divwithonclick: Not keyboard accessible, no semantic meaning. - No
aria-expanded: Screen readers don’t know if menu is open. - Small touch targets: Hard for users with motor impairments.
- Missing
aria-label: Unclear purpose of the toggle button.
✅ The Good (Accessibility-Ready Code)
Why This Works
- Proper Button Element: Inherently keyboard accessible and semantic.
- Dynamic ARIA States:
aria-expandedupdates to reflect current state. - Descriptive Labels: Button label changes from “Open” to “Close” based on state.
- Minimum Touch Size: 44×44px meets WCAG AAA requirements.
Accessibility Checklist
- Use
<button>instead of<div>for the toggle. - Add
aria-expandedthat updates dynamically (true/false). - Include
aria-controlspointing to the menu panel ID. - Update
aria-labelto reflect current state (“Open”/“Close”). - Ensure toggle is at least 44×44px for touch targets.
- Add keyboard support (ESC to close, arrow keys for navigation).
- Use
hiddenattribute instead ofdisplay: nonefor better AT support.
<button
type="button"
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Open navigation menu"
>
<span aria-hidden="true">☰</span>
</button>
<div id="mobile-menu" hidden>
<nav aria-label="Mobile navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</div>
<style>
button {
min-width: 44px;
min-height: 44px;
}
</style>
<script>
const button = document.querySelector('button');
const menu = document.getElementById('mobile-menu');
button.addEventListener('click', () => {
const isOpen = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isOpen);
button.setAttribute('aria-label', isOpen ? 'Open' : 'Close');
menu.hidden = isOpen;
});
</script>
Technical Deep Dive
Screen Reader Announcements
- NVDA: “Open navigation menu, button, collapsed, double tap to activate”
- VoiceOver: “Open navigation menu, button, collapsed”
- TalkBack: “Open navigation menu, button, collapsed, double tap to activate”
Best Practice: Touch Target Size
Mobile navigation buttons must meet WCAG 2.5.5 (Level AAA) touch target requirements: at least 44×44 CSS pixels. This ensures users with motor impairments can easily activate controls. The hamburger button should be clearly labeled with aria-label that updates dynamically to reflect the current state (e.g., “Open menu” vs “Close menu”).
Interactive Behavioral Lab
Interactive Sandbox
🔬 Technical Internals
Understand how this component is processed by the browser and Assistive Technology (AT). This section bridges the gap between visual code and the hidden logic that powers accessibility.
🌲 Accessibility Tree
The data structure used by screen readers to "see" your page. It translates HTML roles and attributes into standardized objects.
⚙️ Event Logic
Expected behavioral standards for keyboard navigation and state transitions. Crucial for users who don't use a mouse.
- Focus: Highlights via
:focus-visible - Activation: Responds to
EnterandSpace - Role: Identified as
Disclosure