Navigation is how people find their way around a site, so a broken menu can make a whole site unusable. The most common problems are dropdowns that open only on hover, menus that keyboard users cannot leave, and markup that tells screen readers the menu is something it is not.
Start with a list of links
Simple navigation needs no ARIA at all.
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/books">Books</a></li>
<li><a href="/events" aria-current="page">Events</a></li>
<li><a href="/visit">Visit us</a></li>
</ul>
</nav>
navcreates a navigation landmark. Name it if there is more than one (lesson 4).- The list tells screen reader users how many items there are: "list, 4 items".
aria-current="page"marks the current page, so screen readers announce "current page". Style it visibly too, with more than colour.
nav [aria-current="page"] {
font-weight: 700;
text-decoration: underline;
text-decoration-thickness: 3px;
}
Why not role="menu"?
ARIA has menu, menubar and menuitem roles. They sound right for a navigation menu, but they describe application menus, like the File and Edit menus in a desktop program. When you use them:
- Screen readers switch into application mode and expect arrow-key navigation;
Tabis expected to leave the whole menu. - Users must be able to move with arrow keys, open submenus with the arrow keys, and type a letter to jump to an item. You must build all of that.
- Links inside lose their link role and are announced as menu items, so users cannot tell they navigate.
Website visitors expect navigation to behave like links: Tab between them, Enter to follow. The APG itself recommends the disclosure pattern for site navigation and reserves menu roles for application-style menus. Use menu roles only for true action menus in web apps, such as a toolbar "More actions" menu, and then follow the APG Menu Button pattern completely.
Dropdowns: the disclosure pattern
A dropdown in site navigation is a disclosure: a button that shows and hides a list of links.
<nav aria-label="Main" class="site-nav">
<ul>
<li><a href="/">Home</a></li>
<li>
<button type="button" aria-expanded="false" aria-controls="nav-books">Books</button>
<ul id="nav-books" hidden>
<li><a href="/books/new">New arrivals</a></li>
<li><a href="/books/ebooks">eBooks and audiobooks</a></li>
<li><a href="/books/reserve">Reserve a book</a></li>
</ul>
</li>
<li><a href="/visit">Visit us</a></li>
</ul>
</nav>
Why a separate button? If the top-level item is also a link, users cannot both follow it and open the dropdown with one control. Either make the top level a button only, and put an "All books" link inside the dropdown, or place a separate small toggle button next to the link, named for example "Books submenu".
The script
The script toggles each dropdown, closes it with Escape, closes it when focus or a click moves elsewhere, and keeps only one open at a time.
const nav = document.querySelector('.site-nav');
const toggles = Array.from(nav.querySelectorAll('button[aria-expanded]'));
function setOpen(button, open) {
button.setAttribute('aria-expanded', String(open));
document.getElementById(button.getAttribute('aria-controls')).hidden = !open;
}
function closeAll(except) {
toggles.forEach((button) => {
if (button !== except) setOpen(button, false);
});
}
toggles.forEach((button) => {
button.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') === 'true';
closeAll(button);
setOpen(button, !open);
});
});
// Escape closes the open dropdown and returns focus to its button.
nav.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
const openButton = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
if (openButton) {
setOpen(openButton, false);
openButton.focus();
}
});
// Close when focus leaves the navigation entirely.
nav.addEventListener('focusout', (event) => {
if (!nav.contains(event.relatedTarget)) closeAll();
});
// Close when the user clicks or taps outside.
document.addEventListener('click', (event) => {
if (!nav.contains(event.target)) closeAll();
});
Keyboard behaviour that results:
| Key | Result |
|---|---|
Tab | Moves through top-level items; into an open dropdown's links; then on to the next item |
Enter or Space on a toggle | Opens or closes the dropdown |
Escape | Closes the dropdown and puts focus on its toggle |
Tab out of the navigation | Closes any open dropdown |
Screen readers announce "Books, button, collapsed", then "expanded" after activation. The links inside are announced as links.
The focusout handler checks event.relatedTarget, which is the element receiving focus. When it is null, focus left the page or went to the browser interface, so closing is still correct.
The APG's disclosure navigation example also adds optional arrow-key support between top-level buttons. It is an enhancement; Tab must always work.
Hover menus
Many designs open dropdowns on hover. If you add hover, keep the button too, so touch, keyboard and voice users can open the dropdown. Success criterion 1.4.13 Content on Hover or Focus (AA) then applies to anything that appears on hover or focus:
- Dismissible: it can be closed without moving the pointer or focus, usually with
Escape. - Hoverable: users can move the pointer over the new content without it disappearing. Leave no gap between trigger and dropdown, and add a short delay before closing.
- Persistent: it stays visible until the user dismisses it, moves away or it is no longer relevant.
Menus that open on hover alone, with no button, fail keyboard and touch users. Clicking to open is the more reliable choice.
Mobile navigation
On small screens, navigation often collapses behind a "Menu" button. That button is another disclosure:
<button type="button" class="menu-toggle" aria-expanded="false" aria-controls="main-nav-list">
Menu
</button>
<nav aria-label="Main">
<ul id="main-nav-list">
<li><a href="/">Home</a></li>
<li><a href="/books">Books</a></li>
</ul>
</nav>
@media (max-width: 47.99em) {
.menu-toggle[aria-expanded="false"] + nav ul {
display: none;
}
}
@media (min-width: 48em) {
.menu-toggle {
display: none;
}
}
Toggle aria-expanded with the same click handler pattern as above. Keep the visible word "Menu" next to any hamburger icon; it is clearer for many people. The Responsive Web Design course covers responsive navigation layouts in depth.
If the mobile menu covers the whole screen like a modal, treat it like one: move focus into it, make the page behind inert and close it with Escape. Lesson 15 shows how with dialog.
Targets and spacing
Navigation links are often small and close together. 2.5.8 Target Size (Minimum) (AA) requires pointer targets of at least 24 by 24 CSS pixels, or enough spacing that a 24 pixel circle centred on each target does not overlap another target. Give links padding rather than relying on the text size.
.site-nav a,
.site-nav button {
display: inline-block;
padding: 0.75rem 1rem;
}
Consistency
3.2.3 Consistent Navigation (AA) asks that navigation repeated across pages appears in the same relative order each time. Keep the order stable across your site, and keep help and contact links in a consistent place too (3.2.6 Consistent Help).
Summary
- Build navigation as a
navwith a list of links, and mark the current page witharia-current="page". - Do not use
menu,menubarormenuitemroles for site navigation; they are for application menus. - Build dropdowns as disclosures: a
buttonwitharia-expandedthat shows and hides a list of links. - Close dropdowns with
Escape, return focus to the toggle, and close when focus or clicks move away. - Hover is only an enhancement; content on hover must be dismissible, hoverable and persistent (1.4.13).
- Give links targets of at least 24 by 24 CSS pixels (2.5.8) and keep navigation consistent across pages.