Introduction
Navigation is how users move through your application. For sighted users, it's visual — menus, tabs, breadcrumbs. For screen reader users, it's landmarks, headings, and link lists. For keyboard users, it's Tab, Enter, and Arrow keys. Good navigation works for all three simultaneously.
The most common navigation patterns — nav bars, dropdown menus, tab panels, breadcrumbs, and pagination — each have specific accessibility requirements defined in the WAI-ARIA Authoring Practices.
This guide covers implementing these patterns with proper semantics, keyboard support, and screen reader announcements.
Key Concepts
Navigation Landmarks
<header>
<nav aria-label="Main">
<ul>{/* Primary navigation */}</ul>
</nav>
</header>
<main>
<nav aria-label="Breadcrumb">
<ol>{/* Breadcrumbs */}</ol>
</nav>
{/* Page content */}
</main>
<aside>
<nav aria-label="Table of Contents">
<ul>{/* Section links */}</ul>
</nav>
</aside>
<footer>
<nav aria-label="Footer">
<ul>{/* Footer links */}</ul>
</nav>
</footer>
Dropdown Menu Pattern
function NavMenu({ items }) {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<nav aria-label="Main">
<ul role="menubar">
{items.map((item, i) => (
<li key={i} role="none">
{item.children ? (
<>
<button role="menuitem" aria-haspopup="true" aria-expanded={openIndex === i}
onKeyDown={e => {
if (e.key === 'ArrowDown') { setOpenIndex(i); }
if (e.key === 'Escape') { setOpenIndex(null); }
}}
onClick={() => setOpenIndex(openIndex === i ? null : i)}>
{item.label}
</button>
{openIndex === i && (
<ul role="menu">
{item.children.map((child, j) => (
<li key={j} role="none">
<a role="menuitem" href={child.href}>{child.label}</a>
</li>
))}
</ul>
)}
</>
) : (
<a role="menuitem" href={item.href}>{item.label}</a>
)}
</li>
))}
</ul>
</nav>
);
}
Practical Examples
1. Breadcrumbs
function Breadcrumbs({ items }: { items: { label: string; href?: string }[] }) {
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-2">
{items.map((item, i) => (
<li key={i} className="flex items-center gap-2">
{i > 0 && <span aria-hidden="true">/</span>}
{item.href ? (
<a href={item.href} className="text-blue-600 hover:underline">{item.label}</a>
) : (
<span aria-current="page" className="font-medium">{item.label}</span>
)}
</li>
))}
</ol>
</nav>
);
}
2. Mobile Navigation
function MobileNav() {
const [open, setOpen] = useState(false);
return (
<>
<button aria-expanded={open} aria-controls="mobile-menu"
aria-label={open ? 'Close menu' : 'Open menu'}
onClick={() => setOpen(!open)}>
{open ? <XIcon /> : <MenuIcon />}
</button>
<nav id="mobile-menu" aria-label="Main" hidden={!open}>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</>
);
}
3. Pagination
function Pagination({ current, total }: { current: number; total: number }) {
return (
<nav aria-label="Pagination">
<ul className="flex gap-2">
<li>
<a href={`?page=${current - 1}`}
aria-disabled={current === 1} className={current === 1 ? 'opacity-50' : ''}>
Previous
</a>
</li>
{Array.from({ length: total }, (_, i) => i + 1).map(page => (
<li key={page}>
<a href={`?page=${page}`}
aria-current={page === current ? 'page' : undefined}
className={page === current ? 'font-bold bg-blue-100 px-3 py-1 rounded' : 'px-3 py-1'}>
{page}
</a>
</li>
))}
<li>
<a href={`?page=${current + 1}`}
aria-disabled={current === total}>
Next
</a>
</li>
</ul>
</nav>
);
}
Best Practices
- ✅ Use <nav> with aria-label when you have multiple navigation regions
- ✅ Use aria-current='page' for the current page in navigation
- ✅ Implement Arrow key navigation within menubars and tab lists
- ✅ Use aria-expanded on buttons that toggle menus
- ✅ Close menus on Escape key and return focus to the trigger
- ❌ Don't use nested <a> tags — invalid HTML and confusing for screen readers
- ❌ Don't make mega-menus that trap keyboard users
Common Pitfalls
- Multiple <nav> elements without aria-label — screen readers list them all as 'navigation'
- Hamburger menus that don't trap focus or announce their state
- Breadcrumbs without aria-current on the current page
- Tab panels where Arrow keys don't move between tabs — only Tab key works
Related Guides
- Keyboard Accessibility Guide — Keyboard patterns for navigation widgets
- ARIA Attributes Guide — ARIA roles and states for navigation
- Screen Reader Testing — Test navigation with screen readers
- WCAG Practical Guide — Navigation-related success criteria