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.
설치
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
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.
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 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.
Severity Scale (this skill)
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
Assistive Technology Context
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.)
Critical: Landmark Structure
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> --><aclass="skip-link"href="#main">Skip to main content</a><!-- Primary navigation --><headerrole="banner"><navaria-label="Main"><ul><li><ahref="/"aria-current="page">Home</a></li><li><ahref="/about">About</a></li><li><ahref="/services">Services</a></li></ul></nav></header><!-- Main content --><mainid="main"tabindex="-1">…</main><!-- Secondary navigation (if present) --><navaria-label="Footer">…</nav>
Rules:
<nav> wraps every navigation region
When multiple <nav> elements are present, every one needs a unique
aria-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"
Critical: Skip Link
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.
The skip link target (#main) needs tabindex="-1" so focus can be moved
to it programmatically even though it is not natively focusable.
Serious: 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.
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).
Serious: Do Not Use role="menu" for Site Navigation
Using 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:
Arrow keys navigate items (not Tab)
Tab exits the menu entirely
Users who expect standard link navigation are confused
Standard site navigation uses native <a> elements in <ul> lists inside
<nav>. No ARIA menu roles needed.
<!-- Wrong — creates wrong AT interaction model --><nav><ulrole="menu"><lirole="menuitem"><ahref="/about">About</a></li></ul></nav><!-- Right — native semantics, no ARIA menu roles --><navaria-label="Main"><ul><li><ahref="/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.
Serious: Dropdown / Disclosure Navigation
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.
Disclosure dropdown (recommended)
<navaria-label="Main"><ul><li><!-- Option A: Top-level item is a link; separate button opens dropdown --><ahref="/services">Services</a><buttontype="button"aria-expanded="false"aria-controls="services-submenu"aria-label="Services submenu"><svgaria-hidden="true"focusable="false"><!-- chevron icon --></svg></button><ulid="services-submenu"hidden><li><ahref="/services/web">Web</a></li><li><ahref="/services/mobile">Mobile</a></li></ul></li><li><!-- Option B: Top-level item is a button that opens dropdown only --><buttontype="button"aria-expanded="false"aria-controls="about-submenu">
About
<svgaria-hidden="true"focusable="false"><!-- chevron --></svg></button><ulid="about-submenu"hidden><li><ahref="/about/team">Team</a></li><li><ahref="/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).
JavaScript for disclosure 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 clickdocument.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;
}
});
});
Hover behaviour
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.
<buttontype="button"id="mobile-menu-toggle"aria-expanded="false"aria-controls="mobile-nav"aria-label="Open main menu"><svgaria-hidden="true"focusable="false"><!-- hamburger icon --></svg></button><navid="mobile-nav"aria-label="Main"hidden><ul><li><ahref="/">Home</a></li><li><ahref="/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).
Moderate: aria-current for Active Navigation States
Beyond aria-current="page", use aria-current for other active states:
<!-- Step indicator --><olaria-label="Order process"><li><ahref="/cart">Cart</a></li><li><ahref="/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: --><navaria-label="Account sections"><ul><li><ahref="/account/profile"aria-current="page">Profile</a></li><li><ahref="/account/security">Security</a></li></ul></nav>
Minor: Navigation Design Principles
5–7 top-level items is the cognitive limit for most users; more creates
findability problems before accessibility ones
Navigation items must have consistent placement across all pages —
moving the nav between pages disoriented users with cognitive disabilities
Navigation icons must have visible text labels — icon-only nav is a
persistent accessibility and usability failure
Do not open new windows or tabs from navigation links without warning
CMS and Framework Notes
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.
Definition of Done Checklist
<nav> landmark wraps every navigation region
Every <nav> has a unique, descriptive aria-label
Skip link: first in DOM, visible on focus, target has tabindex="-1"
aria-current="page" on current page link in every nav
role="menu" / role="menuitem" not used on site navigation
Dropdowns use Disclosure pattern: aria-expanded, aria-controls, hidden
Top-level link and dropdown trigger are separate elements (not one <a>)
Escape closes dropdown and returns focus to trigger
Hover dropdowns: delay prevents immediate close on pointer leave
Breadcrumb: <ol>, aria-label="Breadcrumb", aria-current="page" on last item
Pagination: aria-label on nav, aria-label on prev/next arrows, aria-current on current page
Mobile nav: aria-expanded on toggle, focus moves into nav on open, returns on close
inert applied to background while mobile nav is open
Voice Control tested: all links activatable by speaking visible text
Screen magnification tested at 200%: nav not covering content at high zoom
SPA/framework: focus moved and page title announced after route change
Standards 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/