navigation — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited navigation (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Canonical source:examples/NAVIGATION_ACCESSIBILITY_BEST_PRACTICES.mdinmgifford/ACCESSIBILITY.mdThis skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules when creating or reviewing navigation patterns: primary nav, dropdowns, breadcrumbs, pagination, mobile menus, and in-page navigation. Only load this skill if the project contains navigation components.
Navigation is how users find content. Screen reader users rely on landmark structure to orient themselves; keyboard users rely on predictable tab order and dropdown behaviour; voice control users rely on visible, correctly-labelled interactive elements; magnification users rely on nav that reflows without breaking. Get navigation wrong and everything downstream is harder.
| Level | Meaning |
|---|---|
| Critical | Navigation completely unreachable or creates a keyboard trap |
| Serious | Dropdown only works on hover; role="menu" misused; no aria-current |
| Moderate | Missing aria-label on secondary nav; breadcrumb not labelled |
| Minor | Nav item count exceeds 7; inconsistent aria-expanded update |
Navigation behaves differently across AT. Test with:
| AT | Browser | Key behaviour |
|---|---|---|
| NVDA | Chrome | H key for headings, R for regions, list count announced on nav entry |
| JAWS | Chrome | F6 for frames/regions, Q to move to main; announces nav landmark |
| VoiceOver | Safari (macOS) | VO+U rotor for landmarks and links; announces nav label |
| VoiceOver | Safari (iOS) | Swipe to navigate; nav landmark accessible via rotor |
| TalkBack | Chrome (Android) | Linear swipe navigation; landmarks via local context menu |
| Voice Control | Any | Users navigate by visible link text; all interactive elements need accurate visible labels. aria-label that differs from visible text breaks voice control. |
| Screen magnification | Any | Sticky/fixed navbars shrink the visible viewport. Ensure nav does not obscure content at high zoom (200%, 400%). |
| Reader Mode | Firefox/Edge/Safari | Strips nav from the article view — acceptable. Main content must make sense without it. |
| Edge Read Aloud | Edge | Reads page linearly; nav at top means it is read first — skip link is critical. |
Voice Control note: Dragon NaturallySpeaking and iOS Voice Control navigate by speaking visible link text. If aria-label differs from or overrides visible text, the user cannot activate the link by speaking what they see. The accessible name must contain the visible text. (WCAG 2.5.3 Label in Name.)
Every page must have navigational landmarks. Missing `<nav>` landmark is Serious — screen reader users cannot jump to navigation via the rotor or landmarks list.
<!-- Skip link — always first in <body> -->
<a class="skip-link" href="#main">Skip to main content</a>
<!-- Primary navigation -->
<header role="banner">
<nav aria-label="Main">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
</ul>
</nav>
</header>
<!-- Main content -->
<main id="main" tabindex="-1">…</main>
<!-- Secondary navigation (if present) -->
<nav aria-label="Footer">…</nav>Rules:
<nav> wraps every navigation region<nav> elements are present, every one needs a uniquearia-label — without it, screen readers announce "navigation" for each with no way to distinguish them
aria-label must be short and descriptive: "Main", "Footer", "Breadcrumb","Pagination" — not "Navigation menu" or "Main navigation menu"
The skip link must be the first focusable element in the document and must be visible when focused. A permanently hidden skip link (via display:none) is a Serious issue — it defeats WCAG 2.4.1.
.skip-link {
position: absolute;
top: -100%;
left: 1rem;
padding: 0.5rem 1rem;
background: #000;
color: #fff;
font-weight: bold;
text-decoration: none;
z-index: 9999;
}
.skip-link:focus { top: 1rem; }The skip link target (#main) needs tabindex="-1" so focus can be moved to it programmatically even though it is not natively focusable.
aria-current="page"The current page link must be identified programmatically. Missing `aria-current` is Serious — screen reader users cannot determine where they are in the site without it.
<nav aria-label="Main">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>aria-current="page" is the correct value for navigation items linking to the current page. Use aria-current="true" for other current states (current step in a wizard, current item in a list).
Also provide a visual current-page indicator — never rely on aria-current alone (sighted users cannot see ARIA).
role="menu" for Site NavigationUsing `role="menu"` on site navigation is Serious. This is one of the most common navigation ARIA mistakes.
role="menu" signals a desktop-application-style menu (like a File menu in a word processor). It puts screen readers into application-menu interaction mode:
Standard site navigation uses native <a> elements in <ul> lists inside <nav>. No ARIA menu roles needed.
<!-- Wrong — creates wrong AT interaction model -->
<nav>
<ul role="menu">
<li role="menuitem"><a href="/about">About</a></li>
</ul>
</nav>
<!-- Right — native semantics, no ARIA menu roles -->
<nav aria-label="Main">
<ul>
<li><a href="/about">About</a></li>
</ul>
</nav>The only appropriate use of role="menu" / role="menuitem" in web navigation is for application toolbar menus that genuinely replicate desktop app behaviour with full arrow-key navigation. See the WAI-ARIA APG menubar pattern for that distinct use case.
The recommended pattern for dropdown submenus is the Disclosure pattern (not the APG Menubar pattern). Disclosure is simpler, has broader AT support, and is what most users — including AT users — expect from web navigation.
<nav aria-label="Main">
<ul>
<li>
<!-- Option A: Top-level item is a link; separate button opens dropdown -->
<a href="/services">Services</a>
<button type="button"
aria-expanded="false"
aria-controls="services-submenu"
aria-label="Services submenu">
<svg aria-hidden="true" focusable="false"><!-- chevron icon --></svg>
</button>
<ul id="services-submenu" hidden>
<li><a href="/services/web">Web</a></li>
<li><a href="/services/mobile">Mobile</a></li>
</ul>
</li>
<li>
<!-- Option B: Top-level item is a button that opens dropdown only -->
<button type="button"
aria-expanded="false"
aria-controls="about-submenu">
About
<svg aria-hidden="true" focusable="false"><!-- chevron --></svg>
</button>
<ul id="about-submenu" hidden>
<li><a href="/about/team">Team</a></li>
<li><a href="/about/history">History</a></li>
</ul>
</li>
</ul>
</nav>Do not mix link and dropdown trigger on a single element. If "Services" is both a link to /services and the trigger for a dropdown, keyboard users cannot reach the dropdown without navigating away from the page. Use Option A (separate button) or Option B (button only, link in dropdown).
document.querySelectorAll('[aria-controls][aria-expanded]').forEach(trigger => {
const target = document.getElementById(trigger.getAttribute('aria-controls'));
trigger.addEventListener('click', () => {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
trigger.setAttribute('aria-expanded', String(!expanded));
target.hidden = expanded;
});
// Escape closes the dropdown and returns focus to trigger
target.addEventListener('keydown', e => {
if (e.key === 'Escape') {
trigger.setAttribute('aria-expanded', 'false');
target.hidden = true;
trigger.focus();
}
});
});
// Close on outside click
document.addEventListener('click', e => {
document.querySelectorAll('[aria-controls][aria-expanded="true"]').forEach(trigger => {
if (!trigger.contains(e.target)) {
const target = document.getElementById(trigger.getAttribute('aria-controls'));
trigger.setAttribute('aria-expanded', 'false');
target.hidden = true;
}
});
});If hover also opens dropdowns, the dropdown must not disappear immediately when the pointer moves away — users with motor impairments need time to move the pointer into the submenu. Add a short CSS delay:
.submenu { transition-delay: 0.2s; }Also apply the dropdown open state on focus, not just hover — keyboard users focus the trigger, not hover it.
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/services/web" aria-current="page">Web Design</a></li>
</ol>
</nav><ol> (ordered list) — breadcrumbs have a meaningful sequencearia-label="Breadcrumb" distinguishes it from other nav landmarksaria-current="page" on the current page item<nav aria-label="Pagination">
<ul>
<li>
<a href="/articles?page=1" aria-label="Previous page">
<span aria-hidden="true">←</span>
</a>
</li>
<li><a href="/articles?page=1">1</a></li>
<li><a href="/articles?page=2" aria-current="page" aria-label="Page 2, current">2</a></li>
<li><a href="/articles?page=3">3</a></li>
<li>
<a href="/articles?page=3" aria-label="Next page">
<span aria-hidden="true">→</span>
</a>
</li>
</ul>
</nav>aria-label="Pagination" on the <nav>aria-current="page" on the current page linkaria-label — arrows alone are notmeaningful to screen reader users
aria-label="Page 2, current" avoids ambiguity<button type="button"
id="mobile-menu-toggle"
aria-expanded="false"
aria-controls="mobile-nav"
aria-label="Open main menu">
<svg aria-hidden="true" focusable="false"><!-- hamburger icon --></svg>
</button>
<nav id="mobile-nav" aria-label="Main" hidden>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>const toggle = document.getElementById('mobile-menu-toggle');
const nav = document.getElementById('mobile-nav');
toggle.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
// Update label to reflect current state — action-based label
toggle.setAttribute('aria-label', expanded ? 'Open main menu' : 'Close main menu');
nav.hidden = expanded;
if (!expanded) {
// Move focus to first link in nav when opening
nav.querySelector('a')?.focus();
}
});When the nav closes, return focus to the toggle button. Apply inert to background content while the mobile nav is open (see keyboard/SKILL.md for the inert pattern).
aria-current for Active Navigation StatesBeyond aria-current="page", use aria-current for other active states:
<!-- Step indicator -->
<ol aria-label="Order process">
<li><a href="/cart">Cart</a></li>
<li><a href="/delivery" aria-current="step">Delivery</a></li>
<li>Payment</li>
</ol>
<!-- Tab list (use APG Tabs pattern instead for interactive tabs) -->
<!-- For navigation-style tabs that are links: -->
<nav aria-label="Account sections">
<ul>
<li><a href="/account/profile" aria-current="page">Profile</a></li>
<li><a href="/account/security">Security</a></li>
</ul>
</nav>findability problems before accessibility ones
moving the nav between pages disoriented users with cognitive disabilities
persistent accessibility and usability failure
Drupal: Drupal's Menu module generates <nav> landmark markup. The active trail uses the .is-active class — ensure aria-current="page" is also set programmatically. The Drupal Accessibility Coding Standards require correct landmark structure in contributed modules.
WordPress: Block themes use <nav> landmarks. Check that wp_nav_menu() output includes aria-label when multiple menus are present — it does not by default. Use the nav_menu_args filter to add it.
React/SPA frameworks: Single-page apps must announce page changes after navigation. When a route changes, move focus to the new page's <h1> or a designated skip target, and announce the page title via a live region. Without this, screen reader users hear nothing after navigation.
<nav> landmark wraps every navigation region<nav> has a unique, descriptive aria-labeltabindex="-1"aria-current="page" on current page link in every navrole="menu" / role="menuitem" not used on site navigationaria-expanded, aria-controls, hidden<a>)Escape closes dropdown and returns focus to trigger<ol>, aria-label="Breadcrumb", aria-current="page" on last itemaria-label on nav, aria-label on prev/next arrows, aria-current on current pagearia-expanded on toggle, focus moves into nav on open, returns on closeinert applied to background while mobile nav is openrole="menu" misuse — do not scrape; link to itStandards horizon: These rules target WCAG 2.2 AA. No breaking changes to navigation requirements are anticipated in WCAG 3.0. Monitor: <https://www.w3.org/TR/wcag-3.0/>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.