mgifford/accessibility-skills/navigation

Claude Code and agent skill by mgifford/accessibility-skills

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Load this skill whenever the project contains navigation components — primary navigation menus, dropdown menus, mega menus, breadcrumbs, pagination, mobile hamburger menus, or in-page jump navigation. Under no circumstances create navigation without proper landmark roles, keyboard support, and accessible labels. Absolutely always wrap navigation in <nav> with a unique aria-label.

Install

npx vskill@latest install mgifford/accessibility-skills/navigation

SKILL.md

Navigation Accessibility Skill

Canonical source: examples/NAVIGATION_ACCESSIBILITY_BEST_PRACTICES.md in mgifford/ACCESSIBILITY.md
This 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.


Core Mandate

Navigation must remain understandable and operable with keyboards, touch, speech input, screen readers, screen magnification, text resizing, and browser zoom. 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.


Severity Scale (this skill)

LevelMeaning
CriticalNavigation completely unreachable or creates a keyboard trap
SeriousDropdown only works on hover; role="menu" misused; no aria-current
ModerateMissing aria-label on secondary nav; breadcrumb not labelled
MinorNav item count exceeds 7; inconsistent aria-expanded update

Do not assign severity from the pattern alone — determine the actual effect on navigation and task completion. Consider whether users can reach main content, whether a menu can be opened/operated/closed/left, whether focus becomes trapped or lost, and whether the defect affects every page through a shared component (a shared nav trap can block an entire site).


Assistive Technology Context

Navigation behaves differently across AT. Test with:

ATBrowserKey behaviour
NVDAChromeH key for headings, R for regions, list count announced on nav entry
JAWSChromeF6 for frames/regions, Q to move to main; announces nav landmark
VoiceOverSafari (macOS)VO+U rotor for landmarks and links; announces nav label
VoiceOverSafari (iOS)Swipe to navigate; nav landmark accessible via rotor
TalkBackChrome (Android)Linear swipe navigation; landmarks via local context menu
Voice ControlAnyUsers navigate by visible link text; all interactive elements need accurate visible labels. aria-label that differs from visible text breaks voice control
Screen magnificationAnySticky/fixed navbars shrink the visible viewport at high zoom (200%, 400%)
Reader ModeFirefox/Edge/SafariStrips nav from the article view — acceptable; main content must make sense without it

Voice Control note: Dragon and iOS Voice Control navigate by speaking visible link text. If aria-label differs from visible text, users cannot activate the link by speaking what they see — the accessible name must contain the visible text (WCAG 2.5.3 Label in Name).


Critical: Landmark Structure

html
<a class="skip-link" href="#main-content">Skip to main content</a>
<header>
<a href="/">Service name</a>
<nav aria-label="Primary">
<ul>
<li><a href="/services">Services</a></li>
<li><a href="/guidance">Guidance</a></li>
</ul>
</nav>
</header>
<main id="main-content" tabindex="-1">
<h1>Page title</h1>
</main>
<footer>
<nav aria-label="Footer">…</nav>
</footer>
  • <nav> wraps every major group of links that help users navigate the site,

page, or process — don't wrap every incidental link list in a nav landmark; excess landmarks make region navigation harder

  • When multiple <nav> elements are present, every one needs an

understandable name so users can distinguish them: aria-label="Primary", "Footer", "On this page" — do not include the role word ("Primary navigation landmark" is redundant since the role is already exposed)

  • Use aria-labelledby when a visible heading already names the region
  • Use the same name for repeated navigation landmarks containing the same

links; use different names when landmarks serve different purposes

  • Do not add redundant role="navigation" to <nav>

Missing `<nav>` landmark is Serious — screen reader users cannot jump to navigation via the rotor or landmarks list.


Critical: Skip Link

Must be at or near the beginning of the body, before the content it bypasses, and must be one of the first useful keyboard stops (not necessarily the literal first DOM node). A permanently hidden skip link (display:none, visibility:hidden, or hidden) is a Serious issue — it defeats WCAG 2.4.1.

css
.skip-link {
position: fixed;
z-index: 1000;
inset-block-start: 0.5rem;
inset-inline-start: 0.5rem;
padding: 0.75rem 1rem;
color: #ffffff;
background: #000000;
transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }

Ensure activation moves both viewport and keyboard focus to the destination; use tabindex="-1" on <main> for reliable programmatic focus; ensure sticky headers don't obscure the destination. WCAG also permits other conforming bypass mechanisms (headings, landmarks) depending on the conformance approach.


Serious: Current Location (aria-current)

html
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/guidance" aria-current="page">Guidance</a></li>
</ul>
</nav>

Use the value matching the relationship — not always "true":

  • aria-current="page" — the current page
  • aria-current="step" — the current step in a process
  • aria-current="location" — current location within an environment (e.g., scroll-spy TOC)
  • aria-current="date" / "time" — where applicable
  • aria-current="true" — only when no more specific token applies

Do not use aria-selected for ordinary navigation links — reserve it for widgets whose selected state is part of their role (tabs, listboxes). Always also provide a visual current-item indicator that doesn't rely on colour alone. Missing `aria-current` on a current-location indicator is Serious — screen reader users cannot determine where they are without it.


Serious: Site Navigation Is Not an Application Menu

Using `role="menu"`/`"menuitem"`/`"menubar"` on ordinary site navigation is Serious. Those roles put screen readers into application-menu interaction mode (arrow keys navigate, Tab exits the whole menu) — a mismatch with what users expect from web navigation.

html
<!-- Wrong -->
<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="Primary">
<ul><li><a href="/about">About</a></li></ul>
</nav>

The distinction is based on behaviour and purpose, not visual appearance. role="menu"/"menuitem" is appropriate only for application toolbar/action/ context menus that genuinely implement full arrow-key interaction — see the WAI-ARIA APG menubar pattern for that distinct use case.


Serious: Disclosure (Dropdown) Navigation

Use disclosure buttons for site navigation that shows/hides nested link lists — do not force ordinary navigation into the ARIA menubar pattern.

html
<nav aria-label="Primary" data-disclosure-nav>
<ul>
<li data-disclosure-item>
<a href="/services">Services</a>
<button type="button" aria-expanded="false" aria-controls="services-links">
<span class="visually-hidden">Show Services submenu</span>
<svg aria-hidden="true" focusable="false" viewBox="0 0 20 20">
<path d="m5 7 5 5 5-5" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<ul id="services-links" hidden>
<li><a href="/services/design">Design</a></li>
</ul>
</li>
</ul>
</nav>

Do not mix link and dropdown trigger on a single element — if "Services" is both a link and the dropdown trigger, keyboard users can't reach the dropdown without navigating away. Use a separate button (shown above), or a button-only parent with an "Overview" link as the first child.

js
document.querySelectorAll('[data-disclosure-nav]').forEach((navigation) => {
const buttons = Array.from(navigation.querySelectorAll('[data-disclosure-item] > button[aria-controls]'));
function close(button, restoreFocus = false) {
const panel = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', 'false');
panel.hidden = true;
if (restoreFocus) button.focus();
}
function open(button) {
const panel = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', 'true');
panel.hidden = false;
}
buttons.forEach((button) => {
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
expanded ? close(button) : open(button);
});
const item = button.closest('[data-disclosure-item]');
item.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && button.getAttribute('aria-expanded') === 'true') {
e.preventDefault();
close(button, true);
}
});
item.addEventListener('focusout', (e) => {
if (!item.contains(e.relatedTarget)) close(button);
});
});
document.addEventListener('pointerdown', (event) => {
buttons.forEach((button) => {
const item = button.closest('[data-disclosure-item]');
if (!item.contains(event.target)) close(button);
});
});
});

Requirements: aria-expanded and hidden stay synchronized; Tab/Shift+Tab follow ordinary order; Escape closes and returns focus to the button; moving focus outside the parent item closes its submenu; opening doesn't automatically move focus or change the page; the component works without hover; scope JavaScript to the nav component, not every aria-controls element on the page.

Hover behaviour is optional — click/activation must be sufficient. If hover or focus also reveals content, WCAG 1.4.13 requires it be dismissible (without moving focus/pointer), hoverable (pointer can move onto it), and persistent (stays until hover/focus removed or dismissed). A CSS transition-delay alone does not satisfy hoverable/persistent behaviour.


Moderate: Breadcrumbs

html
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/services">Services</a></li>
<li aria-current="page">Web accessibility</li>
</ol>
</nav>

Use <ol> — order is meaningful. Hide decorative separators from AT (preferably via CSS). The current item may be plain text or a link — don't create a link that performs no useful navigation. Keep the visible breadcrumb consistent with the actual page hierarchy. WCAG does not require breadcrumbs on every site.


Moderate: Pagination

html
<nav aria-label="Pagination">
<ul>
<li><a href="?page=1" rel="prev">Previous</a></li>
<li><a href="?page=1" aria-label="Page 1">1</a></li>
<li><a href="?page=2" aria-label="Page 2" aria-current="page">2</a></li>
<li><a href="?page=3" rel="next">Next</a></li>
</ul>
</nav>

Use visible "Previous"/"Next" text where space permits; if only an arrow is visible, the accessible name must contain "Previous"/"Next". Ensure the current page has a visible non-colour distinction. Do not create links for unavailable Previous/Next actions. Preserve filters, search terms, and sort order in pagination URLs.


Moderate: Responsive (Mobile) Navigation

Treat a typical responsive menu as a non-modal disclosure unless the design genuinely requires a modal navigation drawer — do not apply inert to the rest of the page for an ordinary hamburger menu.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/mgifford/accessibility-skills/navigation/badge)](https://verified-skill.com/skills/mgifford/accessibility-skills/navigation)

Something wrong with this skill or its page? Tell us.