Skip to content
Back to skills

Navigation

ASecurity

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.

  • 47 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 26, 2026
designjavascriptgojavareactawsgitsecurity

Works with

  • cli

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add mgifford/accessibility-skills --skill navigation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Navigation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Navigation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mgifford-navigation/badge)](https://www.skillsdirectory.com/skills/mgifford-navigation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: navigation
description: >
  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.

```html
<!-- 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
- 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.

```css
.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.

---

## 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.

```html
<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).

---

## 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.

```html
<!-- 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](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/)
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)

```html
<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).

### JavaScript for disclosure dropdown

```js
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;
    }
  });
});
```

### 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:

```css
.submenu { transition-delay: 0.2s; }
```

Also apply the dropdown open state on focus, not just hover — keyboard users
focus the trigger, not hover it.

---

## Moderate: Breadcrumbs

```html
<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>
```

- Use `<ol>` (ordered list) — breadcrumbs have a meaningful sequence
- `aria-label="Breadcrumb"` distinguishes it from other nav landmarks
- `aria-current="page"` on the current page item
- The current item may be a link or plain text — both are acceptable

---

## Moderate: Pagination

```html
<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 link
- Previous/Next arrows need descriptive `aria-label` — arrows alone are not
  meaningful to screen reader users
- Page numbers: `aria-label="Page 2, current"` avoids ambiguity

---

## Moderate: Mobile Navigation (Hamburger)

```html
<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>
```

```js
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:

```html
<!-- 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>
```

---

## 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](https://www.drupal.org/docs/getting-started/accessibility/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
* [ ] Tested: NVDA+Chrome, JAWS+Chrome, VoiceOver+Safari

---

## Key WCAG Criteria

* 2.4.1 Bypass Blocks (A) — **Serious if skip link absent or broken**
* 2.4.3 Focus Order (A)
* 2.4.5 Multiple Ways (AA) — navigation + search or sitemap
* 2.4.7 Focus Visible (AA)
* 2.4.11 Focus Appearance (AA, WCAG 2.2)
* 2.5.3 Label in Name (A) — **Serious for voice control if violated**
* 4.1.2 Name, Role, Value (A) — **Serious if `aria-expanded` not updated**

---

## References

* [Full best practices guide](https://github.com/mgifford/ACCESSIBILITY.md/blob/main/examples/NAVIGATION_ACCESSIBILITY_BEST_PRACTICES.md)
* [Curated resources — navigation](../resources/by-topic/navigation.md)
* [WAI Menus Tutorial](https://www.w3.org/WAI/tutorials/menus/) — structure, fly-out, application menus
* [WAI-ARIA APG — Disclosure Navigation](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/) — recommended dropdown pattern
* [WAI-ARIA APG — Menubar](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) — for application toolbar menus only
* [Pope Tech — Accessible navigations and sub-menus](https://blog.pope.tech/2024/01/30/how-to-create-accessible-navigations-and-sub-menus/)
* [Adrian Roselli — Be careful using menu](https://adrianroselli.com/2023/05/be-careful-using-menu.html) — `role="menu"` misuse — do not scrape; link to it
* [Drupal Accessibility Coding Standards](https://www.drupal.org/docs/getting-started/accessibility/accessibility-coding-standards)
* [WCAG 2.2 Understanding 2.4.1 Bypass Blocks](https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html)
* [WCAG 2.2 Understanding 2.5.3 Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html)

> **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/>

Files in this skill

  • README.md354 B
  • SKILL.md17.3 KB
  • SYNC.md594 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.