Accessible navigation and skip links
Navigation is one of the first things a screen reader or keyboard user encounters. A well-structured, accessible navigation system makes the difference between a usable site and an unusable one.
Skip links - bypassing repeated content
Keyboard users must tab through every link in the header and navigation before reaching the main content on every page load. A skip link solves this by allowing them to jump directly to the main content.
<!-- As the very first element inside <body> -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>
<nav>
<!-- Navigation links -->
</nav>
</header>
<main id="main-content" tabindex="-1">
<!-- Page content -->
</main>.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 0.75rem 1.5rem;
background: #005fcc;
color: #ffffff;
font-weight: 600;
z-index: 9999;
transition: top 0.2s ease;
}
.skip-link:focus {
top: 0;
}Why
tabindex="-1"on<main>? Without it, some browsers will scroll to the anchor but not move keyboard focus. Addingtabindex="-1"ensures focus actually moves to the main content area.
Accessible <nav> structure
Label multiple navigations
When a page has more than one <nav>, each must have a unique label:
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<nav aria-label="Footer">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
</ul>
</nav>Indicate the current page
Use aria-current="page" to tell screen readers which link represents the currently active page:
<nav aria-label="Primary">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>This is far more reliable than relying on visual styles like “active” classes, which provide no semantic meaning.
Breadcrumb navigation
Breadcrumbs help users understand their position in the site hierarchy:
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li><a href="/products/shoes" aria-current="page">Shoes</a></li>
</ol>
</nav>/* Visual separators using CSS - not announced by screen readers */
nav[aria-label="Breadcrumb"] ol {
display: flex;
list-style: none;
padding: 0;
}
nav[aria-label="Breadcrumb"] li + li::before {
content: '/';
margin: 0 0.5rem;
color: #666;
}Note: Use
<ol>(ordered list) for breadcrumbs since the order matters. The CSS-generated separator (/) is invisible to screen readers, avoiding noisy “slash” announcements.
Scenarios and Edge Cases
Mega menus
Large navigation menus (mega menus) require careful implementation:
- Use
aria-expandedon the trigger button to indicate menu state. - Add
aria-haspopup="true"on the trigger to indicate a submenu exists. - Allow
Escapeto close the menu and return focus to the trigger. - Use arrow keys for navigation within the menu (following the WAI-ARIA Menu pattern).
<nav aria-label="Primary">
<ul role="menubar">
<li role="none">
<button role="menuitem" aria-haspopup="true" aria-expanded="false">
Products
</button>
<ul role="menu" hidden>
<li role="none"><a role="menuitem" href="/shoes">Shoes</a></li>
<li role="none"><a role="menuitem" href="/bags">Bags</a></li>
</ul>
</li>
</ul>
</nav>Mobile hamburger menus
- The toggle button must use
aria-expandedandaria-controls. - The hidden menu must have
aria-hidden="true"when closed. - Focus must move into the menu when opened and return to the toggle button when closed.
<button
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Open navigation menu"
>
<span class="hamburger-icon" aria-hidden="true">Menu</span>
</button>
<nav id="mobile-menu" aria-label="Mobile navigation" hidden>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>Single-page application (SPA) navigation
In SPAs, route changes don’t trigger a page reload, so screen readers have no way of knowing content has changed - you have to announce it and move focus yourself. That’s a big enough topic to deserve its own space: see focus management in SPAs for the full pattern, including the framework-specific React and Angular examples.
Pagination
Paginated lists should announce the current page and total within the navigation:
<nav aria-label="Pagination">
<ul>
<li><a href="/page/1">1</a></li>
<li><a href="/page/2" aria-current="page">2</a></li>
<li><a href="/page/3">3</a></li>
</ul>
</nav>Resources
For how <nav> fits alongside the rest of the page’s landmarks, see HTML landmarks for accessibility. For the ARIA attributes used throughout this post (aria-expanded, aria-haspopup, aria-current), see ARIA roles, states, and properties.
If you found this helpful, share it with someone who's building for the web.