Navigation is the part of your site every visitor touches, and it is where accessibility bugs are most expensive: if someone cannot move between pages, nothing else you built matters. Yet accessible navigation menus are also one of the most over-engineered components on the web. Developers reach for JavaScript, ARIA, and custom widgets when plain HTML and a few well-placed attributes would do the job better. This guide covers the five things that actually matter: landmarks, keyboard operability, skip links, accessible dropdown and hamburger menus, and current-page indication.
Everything here maps to WCAG 2.2 Level A and AA, the baseline referenced through EN 301 549 and enforced by the European Accessibility Act from 28 June 2025. None of it requires a framework. Most of it is markup you can ship today.
Wrap navigation in landmarks so it can be found and skipped
A screen reader user rarely reads a page top to bottom. They jump by landmark, and navigation is the landmark they reach for most. Wrap your primary menu in a <nav> element and it is exposed automatically as a navigation landmark, listed in the rotor or landmark menu, and announced by name.
If a page has more than one <nav> (a primary menu and a footer menu, say), give each an accessible name so they are distinguishable. Use aria-label: <nav aria-label="Primary"> and <nav aria-label="Footer">. Do not include the word "navigation" in the label, because the screen reader already announces the role, so "Primary navigation navigation" is what users hear otherwise.
Inside the <nav>, use a real list: <ul> with <li> items wrapping <a href> links. The list count ("list of 6 items") gives users a sense of scale before they commit to reading it. This is also the structure assistive tech expects, which means you write zero ARIA to get correct semantics.
Keyboard operability: native elements first
Every link and control in your menu must be reachable and operable with the keyboard alone (WCAG 2.1.1 Keyboard, Level A). The shortcut to getting this right is using native elements. An <a href> is focusable, activates on Enter, and is announced as a link without any work. The moment you build a menu item out of <div onclick>, you inherit the browser's entire job: adding tabindex, wiring up key handlers, and exposing a role. Avoid it.
Tab order must follow reading order (WCAG 2.4.3 Focus Order, Level A). Because the browser derives tab order from the DOM, keep your markup in the order people read it. If you reorder menu items visually with Flexbox order, focus will not follow, and a keyboard user watches the highlight jump around unpredictably. Never use positive tabindex values to patch this. Reorder the markup instead. For a deeper treatment of focus management, see the keyboard accessibility guide.
Focus must also be visible (WCAG 2.4.7 Focus Visible, Level AA) and, under WCAG 2.2, not obscured by sticky headers or cookie banners (2.4.11 Focus Not Obscured (Minimum), Level AA). Never ship outline: none without a replacement. A strong custom :focus-visible style that meets the 3:1 non-text contrast requirement against its background keeps sighted keyboard users oriented. If your nav is position: sticky, add scroll-margin-top so a focused link below it is not hidden underneath.
Add a skip link so keyboard users can bypass the menu
A skip link lets keyboard and screen reader users jump past the navigation straight to the main content (WCAG 2.4.1 Bypass Blocks, Level A). Without one, every Tab into a new page means stepping through every menu item before reaching the article. With a 30-link mega menu, that is brutal.
The implementation is four lines. Make the first focusable element in the body an anchor pointing at your main region: <a class="skip-link" href="#main">Skip to main content</a>, and give your content <main id="main" tabindex="-1">. The tabindex="-1" ensures focus actually lands on <main> when activated, which some browsers otherwise skip.
Hide the link visually until it receives focus rather than with display: none, which would remove it from the tab order entirely. Position it off-screen and reveal it on :focus. The classic pattern uses absolute positioning with a negative top that flips to 0 on focus, so the link appears at the top-left corner the instant a keyboard user tabs to it and stays invisible to everyone else.
Accessible dropdown and hamburger menus
Most "dropdown" navigation is not a menu in the ARIA sense, and treating it like one causes bugs. The ARIA menu/menuitem pattern is for application menus (think a desktop app's File menu) where arrow keys move between items and Tab exits the whole widget. A site navigation made of links should not use those roles. Use a disclosure pattern instead.
A disclosure dropdown is a <button> that toggles a panel of links. Wire it with aria-expanded reflecting state ("false" when closed, "true" when open) and aria-controls pointing at the panel's id. Toggle the panel's visibility and the aria-expanded value together in your click handler. Add an Escape key handler that closes the panel and returns focus to the button, and close on outside click. Because the contents are ordinary links, Tab moves through them naturally and no arrow-key logic is required.
The hamburger menu
A hamburger toggle is the same disclosure pattern with one extra requirement: a text name. An icon-only <button> with no label is silent to a screen reader. Add aria-label="Menu" (or visually hidden text) and keep aria-expanded in sync. Do not use aria-haspopup="true" here, since that implies a menu widget you are not building. Under WCAG 2.2, the toggle must also be at least 24 by 24 CSS pixels (2.5.8 Target Size (Minimum), Level AA), which is easy to miss with a tightly cropped icon button. For the full reasoning on roles versus disclosures, the ARIA best practices guide goes deeper.
Indicate the current page
Users need to know where they are. The accessible way to mark the active menu item is aria-current="page" on the link to the current URL: <a href="/pricing" aria-current="page">Pricing</a>. Screen readers announce this as "current page," giving an unambiguous signal that a color change alone cannot.
Crucially, do not rely on color alone to show the active state (WCAG 1.4.1 Use of Color, Level A). Pair aria-current with a non-color cue such as an underline, a bold weight, or a left border, and make sure any color you do use clears 3:1 contrast against its background for the indicator and 4.5:1 for the text. You can target it in CSS with the attribute selector [aria-current="page"], so the visual and programmatic states stay in lockstep from a single source of truth.
Test it in ten minutes
You do not need specialized tooling to catch most navigation failures. Run these checks before shipping:
- Unplug the mouse. Tab through the whole nav. Can you reach and activate every link, open and close every dropdown, and operate the hamburger? Is focus always visible and never hidden behind a sticky header?
- Tab once on a fresh page load. Does a skip link appear first?
- Open the landmark menu in a screen reader (VoiceOver, NVDA, or JAWS) and confirm each
<nav>has a distinct name. - Navigate to a sub-page and confirm the active item announces as "current page."
- Shrink the viewport and confirm the hamburger toggle announces its state and meets the 24px target size.
For an automated baseline, run a scan with AccessScan to flag missing names, low contrast, and broken landmarks before you start manual testing. Automated tools only catch a portion of accessibility issues, so the keyboard and screen reader passes above are not optional.