Skip to content
Accessible Navigation & Menus

Accessible Navigation & Menus

DodaTech Updated Jun 20, 2026 11 min read

Navigation is how users move through your site — and if it’s not accessible, users with disabilities can’t find content, complete tasks, or understand your site’s structure. Accessible navigation means semantic landmarks, clear link text, keybord-operable menus, and persistent wayfinding cues.

What You’ll Learn

By the end of this tutorial, you’ll understand HTML5 landmark elements, skip links, accessible breadcrumbs, responsive hamburger menus with proper ARIA, aria-current for active states, keyboard-operable mega menus, search accessibility, accessible pagination, and site map best practices.

Why Accessible Navigation Matters

Navigation is the first thing screen reader users encounter on every page. If it’s not structured with landmarks and clear link text, they can’t skip to content or understand where they are. According to the WebAIM Million survey, “empty links” and “missing form labels” are the most common accessibility failures, and navigation components are frequent offenders. At DodaTech, Doda Browser includes a navigation landmark viewer that shows the page structure hierarchy.

Accessible Navigation Learning Path

    flowchart LR
  A[Accessibility Overview] --> B[WCAG Compliance]
  B --> C[Keyboard Navigation]
  C --> D[Accessible Navigation]
  D:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: HTML semantics, understanding of ARIA roles, keyboard navigation knowledge from our Keyboard Navigation tutorial.

Landmark Elements

HTML5 landmark elements help screen reader users navigate the page structure:

<header role="banner">
  <!-- Site branding, logo, global navigation -->
  <a href="/" aria-label="DodaTech home">
    <img src="logo.svg" alt="">
  </a>
</header>

<nav aria-label="Main navigation">
  <!-- Primary navigation links -->
</nav>

<main>
  <h1>Page title</h1>
  <!-- Primary page content -->
</main>

<aside aria-label="Related articles">
  <!-- Complementary content -->
</aside>

<footer role="contentinfo">
  <!-- Copyright, legal links, contact info -->
</footer>

Why explicit role attributes on landmarks? While HTML5 elements have implicit roles, adding explicit roles ensures older browsers and screen readers recognize them. The W3C recommends this approach for broad compatibility.

Multiple Landmarks of the Same Type

When you have multiple <nav> or <section> elements, label them with aria-label:

<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

<nav aria-label="Footer">
  <ul>
    <li><a href="/privacy">Privacy Policy</a></li>
    <li><a href="/terms">Terms of Service</a></li>
  </ul>
</nav>

Screen reader navigation: Users can press D (NVDA) or use the Rotor (VoiceOver) to jump between landmarks. Without labels, both <nav> elements show as “Navigation” — the user can’t distinguish them.

Skip Links

Skip links let keyboard users bypass navigation and jump to main content. They should be the first focusable element on the page:

<style>
  .skip-link {
    position: absolute;
    top: -100%;
    left: 8px;
    padding: 12px 24px;
    background: #005fcc;
    color: white;
    z-index: 10000;
    border-radius: 0 0 4px 4px;
    text-decoration: none;
    font-weight: 600;
  }
  .skip-link:focus {
    top: 0;
  }
</style>

<a href="#main-content" class="skip-link">Skip to main content</a>

<header><!-- nav, logo, etc. --></header>

<main id="main-content" tabindex="-1">
  <h1>Main Content</h1>
</main>

Why tabindex="-1" on <main>? <main> isn’t focusable by default in some browsers. Adding tabindex="-1" ensures programmatic focus works when the skip link is activated.

Breadcrumbs

Breadcrumbs should be wrapped in a <nav> with aria-label="Breadcrumb" and use aria-current="page" for the current page:

<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/products/laptops">Laptops</a></li>
    <li><a href="/products/laptops/gaming">Gaming Laptops</a></li>
    <li aria-current="page">DodaTech Pro X1</li>
  </ol>
</nav>

Screen reader behavior: VoiceOver announces “Breadcrumb, 5 items, current page: DodaTech Pro X1, end of breadcrumb.” The aria-current="page" tells the user “you are here.”

/* Breadcrumb styling with separator (don't use > — it can be confusing) */
nav[aria-label="Breadcrumb"] ol {
  list-style: none;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

nav[aria-label="Breadcrumb"] li:not(:last-child)::after {
  content: "›";
  margin-left: 4px;
  color: #666;
}

Responsive Hamburger Menus

Hamburger menus are problematic for accessibility when not implemented correctly. Here’s a fully accessible version:

<nav aria-label="Main navigation">
  <button aria-expanded="false" aria-controls="nav-menu"
          id="nav-toggle" class="nav-toggle">
    <span class="sr-only">Menu</span>
    <span class="hamburger-icon" aria-hidden="true">
      <span></span><span></span><span></span>
    </span>
  </button>

  <ul id="nav-menu" class="nav-menu" role="list" hidden>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/blog">Blog</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

<style>
  .sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    border: 0;
  }

  .nav-toggle {
    display: none; /* shown on mobile via media query */
    padding: 12px;
    background: none;
    border: none;
    cursor: pointer;
  }

  @media (max-width: 768px) {
    .nav-toggle { display: block; }
    .nav-menu:not([hidden]) {
      display: flex;
      flex-direction: column;
    }
  }
</style>

<script>
const toggle = document.getElementById('nav-toggle');
const menu = document.getElementById('nav-menu');

toggle.addEventListener('click', () => {
  const expanded = toggle.getAttribute('aria-expanded') === 'true';
  toggle.setAttribute('aria-expanded', !expanded);
  menu.hidden = expanded;
});

// Close menu on Escape
menu.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    menu.hidden = true;
    toggle.setAttribute('aria-expanded', 'false');
    toggle.focus();
  }
});
</script>

Key accessibility features:

  • aria-expanded tells screen readers whether the menu is open or closed
  • aria-controls associates the button with the menu
  • The visible label is “Menu” (screen reader) while the hamburger icon is hidden with aria-hidden
  • Escape closes the menu and returns focus
  • The menu is hidden with the hidden attribute (not display: none toggle in CSS)

aria-current for Active State

aria-current indicates the current item in a set — and it has several values:

ValueMeaningUse Case
pageCurrent page in a set of pagesNavigation links, pagination
stepCurrent step in a processMulti-step forms, checkout
locationCurrent item in a contextBreadcrumbs
dateCurrent date in a setCalendar widgets
timeCurrent time in a setSchedule
trueDefault — same as pageGeneral current item
<!-- aria-current="page" in navigation -->
<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/products" aria-current="page">Products</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

<!-- aria-current="step" in a multi-step form -->
<nav aria-label="Checkout progress">
  <ol>
    <li aria-current="step">Cart</li>
    <li>Shipping</li>
    <li>Payment</li>
    <li>Confirmation</li>
  </ol>
</nav>

Screen reader announcement: “Products, link, current page.” Users know exactly where they are.

Mega Menus

Mega menus (large dropdowns with multiple columns and groups) need careful keyboard handling:

<nav aria-label="Main">
  <ul>
    <li>
      <button aria-expanded="false" aria-controls="products-menu">
        Products
      </button>
      <div id="products-menu" role="region"
           aria-label="Products submenu" hidden>
        <div class="mega-grid">
          <section aria-labelledby="heading-software">
            <h3 id="heading-software">Software</h3>
            <ul>
              <li><a href="/doda-browser">Doda Browser</a></li>
              <li><a href="/durga-antivirus">Durga Antivirus Pro</a></li>
              <li><a href="/dodazip">DodaZIP</a></li>
            </ul>
          </section>
          <section aria-labelledby="heading-services">
            <h3 id="heading-services">Services</h3>
            <ul>
              <li><a href="/consulting">Consulting</a></li>
              <li><a href="/support">Support</a></li>
              <li><a href="/training">Training</a></li>
            </ul>
          </section>
        </div>
      </div>
    </li>
    <li><a href="/pricing">Pricing</a></li>
  </ul>
</nav>

Keyboard behavior for mega menus: Enter/Space toggles the menu. Escape closes it. Arrow keys move between items within the submenu. Tab moves to the next focusable element (exits the menu).

Search Accessibility

The search feature is one of the most important navigation tools:

<form role="search" aria-label="Site search">
  <label for="search-input" class="sr-only">Search</label>
  <input type="search" id="search-input" name="q"
         placeholder="Search products, articles..."
         aria-describedby="search-hint">
  <button type="submit" aria-label="Search">
    <span aria-hidden="true">🔍</span>
  </button>
  <p id="search-hint" class="sr-only">
    Type your search term and press Enter
  </p>
</form>

<script>
// Real-time search results with accessibility
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');

searchInput.addEventListener('input', debounce(async () => {
  const query = searchInput.value.trim();
  if (query.length < 2) {
    searchResults.innerHTML = '';
    return;
  }

  const results = await fetchSearchResults(query);

  if (results.length > 0) {
    searchResults.innerHTML = `
      <ul role="listbox" aria-label="Search results">
        ${results.map((r, i) => `
          <li role="option" aria-selected="false">
            <a href="${r.url}">${r.title}</a>
          </li>
        `).join('')}
      </ul>
    `;

    // Announce result count
    searchResults.setAttribute('aria-live', 'polite');
    searchResults.innerHTML +=
      `<span class="sr-only">${results.length} results found for "${query}"</span>`;
  }
}, 300));
</script>

Pagination

Pagination controls must convey the current page and allow keyboard navigation:

<nav aria-label="Pagination">
  <ul class="pagination">
    <li>
      <a href="/blog/page/3" aria-label="Previous page"
         rel="prev">&laquo; Previous</a>
    </li>
    <li><a href="/blog/page/1" aria-label="Page 1">1</a></li>
    <li><a href="/blog/page/2" aria-label="Page 2">2</a></li>
    <li><a href="/blog/page/3" aria-label="Page 3">3</a></li>
    <li aria-current="page">
      <span aria-label="Current page, page 4">4</span>
    </li>
    <li><a href="/blog/page/5" aria-label="Page 5">5</a></li>
    <li>
      <a href="/blog/page/5" aria-label="Next page" rel="next">
        Next &raquo;
      </a>
    </li>
  </ul>
</nav>

Screen reader announcement: “Pagination, 5 pages, current page: page 4. Links: previous page, page 1, page 2, page 3, page 5, next page.”

Accessible Navigation Checklist

## Navigation Accessibility Checklist

### Landmarks
- [ ] `<header role="banner">` at top
- [ ] `<nav aria-label="Main">` for primary navigation
- [ ] `<main>` for primary content
- [ ] `<footer role="contentinfo">` at bottom
- [ ] All landmarks have unique labels if multiple of same type

### Skip Links
- [ ] Skip link is first focusable element
- [ ] Skip link is visible on focus
- [ ] Target element has `tabindex="-1"`
- [ ] Focus moves to main content on activation

### Links
- [ ] Link text describes the destination
- [ ] No "click here" or "read more" links
- [ ] Active page marked with `aria-current="page"`
- [ ] Same visual pages have same URL

### Dropdowns / Mega Menus
- [ ] Toggle has `aria-expanded` and `aria-controls`
- [ ] Submenu uses `role="region"` with `aria-label`
- [ ] Escape closes the menu
- [ ] Focus returns to toggle when menu closes
- [ ] Arrow keys navigate items within the menu

### Search
- [ ] Search form uses `role="search"`
- [ ] Search input has visible label on focus
- [ ] Results are announced via `aria-live`
- [ ] Search button has accessible name

### Pagination
- [ ] Wrapped in `<nav aria-label="Pagination">`
- [ ] Current page has `aria-current="page"`
- [ ] Previous/next have descriptive labels
- [ ] Page numbers clearly labeled

Common Navigation Mistakes

1. “Click Here” Links

“Click here” is meaningless to screen reader users who navigate by links. They hear “Click here, click here, Products, click here” — entirely unhelpful. Use descriptive anchor text.

2. Unlabeled Navigation Landmarks

Multiple <nav> elements without aria-label all show as “Navigation” — users can’t tell them apart.

3. Missing Active Page Indicator

Without aria-current="page", screen reader users can’t tell which page they’re on. The visual “active” state (bold text, underline) means nothing to them.

4. Hamburger Menu Without ARIA

A hamburger button without aria-expanded doesn’t tell screen readers whether the menu is open or closed.

5. Keyboard Traps in Dropdowns

Dropdowns that don’t close on Escape, or that trap Tab inside them, prevent keyboard users from moving past the navigation.

6. Breadcrumb Without Current Page

Breadcrumbs should end with the current page marked with aria-current="page", not as a link.

7. Mega Menus Not Closing on Blur

Mega menus that stay open when focus leaves require mouse interaction to close. Use blur events or focusout listeners.

Practice Questions

1. What does aria-current="page" do and where should it be used?

It indicates the current page in a navigation set. Screen readers announce “current page” after the link text. Use it on the <a> or <span> representing the current page in nav, breadcrumbs, or pagination.

2. Why should you use aria-label on <nav> elements?

When you have multiple navigations (main nav, footer nav, breadcrumb), aria-label gives each a unique name so screen reader users can distinguish them.

3. What are the keyboard requirements for a responsive hamburger menu?

Enter/Space toggles the menu, Escape closes it, focus returns to the toggle button after closing, and aria-expanded reflects the state.

4. Why is it important that skip links are the first focusable element?

Because pressing Tab on page load should immediately offer the option to skip navigation. If anything comes before the skip link, keyboard users waste keystrokes.

5. Challenge: Build a fully accessible mega menu with at least 3 sections (each with sub-items). It should open on button click (not hover), navigate with arrow keys, close on Escape, work with keyboard only, and have proper ARIA attributes throughout.

Real-World Task

Audit your website’s navigation. Check: all links have descriptive text, landmarks are labeled, the current page is indicated with aria-current, dropdowns are keyboard accessible, the search is properly labeled, and breadcrumbs mark the current page. Fix all issues.

FAQ

Should I use <nav> or role="navigation"?
Both. Use <nav> for semantic HTML and add role="navigation" for backward compatibility with older browsers and screen readers.
Do all links need aria-current?
No. Only the link representing the current page or section. In a list of nav items, only one should have aria-current="page".
How do I handle mega menus on mobile?
Use the same hamburger pattern: toggle button with aria-expanded, submenu with role="region", and keyboard support. Mega menu columns should stack vertically on small screens.
Should breadcrumbs be in <ol> or <ul>?
<ol> (ordered list) — breadcrumbs represent a sequence from root to current page. The order matters.
Is a sitemap required for accessibility?
No, but it helps. A sitemap provides an alternative way to navigate for users who may have difficulty with complex navigation structures.

Try It Yourself

Let’s build a navigation accessibility testing tool:

// nav-audit.js — audit navigation accessibility
function auditNavigation() {
  const issues = [];

  // Check landmarks
  const navs = document.querySelectorAll('nav');
  navs.forEach(nav => {
    const label = nav.getAttribute('aria-label');
    const labelledby = nav.getAttribute('aria-labelledby');
    if (!label && !labelledby) {
      issues.push({
        type: 'warning',
        element: nav.outerHTML.substring(0, 60),
        message: '<nav> without aria-label — screen readers can\'t distinguish it'
      });
    }
  });

  // Check links
  const links = document.querySelectorAll('a');
  links.forEach(link => {
    const text = link.textContent.trim();
    const href = link.getAttribute('href');

    if (text === '' && !link.querySelector('img[alt]')) {
      issues.push({
        type: 'critical',
        element: href,
        message: 'Empty link — screen reader can\'t determine purpose'
      });
    }

    if (/^(click here|read more|learn more|link)$/i.test(text)) {
      issues.push({
        type: 'critical',
        element: href,
        message: `Generic link text: "${text}" — no context about destination`
      });
    }
  });

  // Check skip links
  const skipLink = document.querySelector('a[href^="#"]:first-child');
  if (!skipLink || !skipLink.className.includes('skip')) {
    issues.push({
      type: 'info',
      element: 'page top',
      message: 'No skip link detected — keyboard users must tab through all navigation'
    });
  }

  // Check active page indicators
  const currentLinks = document.querySelectorAll('[aria-current]');
  if (currentLinks.length === 0) {
    issues.push({
      type: 'info',
      element: 'navigation',
      message: 'No aria-current="page" found — current page not indicated to screen readers'
    });
  }

  console.table(issues);
  return issues;
}

auditNavigation();

Expected output: A console table showing all navigation issues found, categorized by severity (critical, warning, info).

What’s Next

Congratulations on completing this Accessible Navigation tutorial! Here’s where to go from here:

  • Practice daily — Test navigation on every page you build
  • Build a project — Create an accessible navigation component library
  • Explore related topics — Learn accessibility testing next
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro