Blog

Accessible Navigation Menus: Dropdown and Mega Menu Implementation

TestParty
TestParty
September 28, 2025

Navigation is how users find their way around your site—and when navigation isn't accessible, users with disabilities can't get anywhere at all. I've tested sites where the navigation looked beautiful but was completely unusable with a keyboard, or where screen reader users heard "link, link, link" with no context about menu structure.

Building accessible navigation requires understanding how different users interact with menus and implementing patterns that work for everyone. This guide covers everything from simple nav bars to complex mega menus.

Q: How do I make navigation menus accessible?

A: Accessible navigation requires: semantic HTML (nav element with proper list structure), keyboard operability (Tab to navigate, Enter/Space to activate, Escape to close), visible focus indicators, clear menu structure for screen readers, proper ARIA attributes for dropdowns (aria-expanded, aria-haspopup), and mobile-friendly touch targets. Complex menus need arrow key navigation and careful focus management.

Semantic HTML Foundation

Start with proper semantic structure:

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

`<nav>` element: Identifies navigation landmark. Screen reader users can jump directly to navigation.

`aria-label`: Distinguishes between multiple navigation regions ("Main navigation" vs. "Footer navigation").

`<ul>` and `<li>`: Conveys list structure. Screen readers announce "list, 4 items" helping users understand scope.

Multiple Navigation Regions

When you have multiple nav elements, label them distinctly:

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

<nav aria-label="Breadcrumb">
  <!-- Breadcrumb navigation -->
</nav>

<nav aria-label="Footer">
  <!-- Footer navigation -->
</nav>

Screen reader users navigating by landmarks hear "Main navigation," "Breadcrumb navigation," etc.

Current Page Indication

Indicate the current page for orientation:

<nav aria-label="Main navigation">
  <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="page" tells screen readers this link leads to the current page.

HTML Structure

<nav aria-label="Main navigation">
  <ul class="nav-menu">
    <li><a href="/">Home</a></li>
    <li class="has-dropdown">
      <button
        aria-expanded="false"
        aria-haspopup="true"
        aria-controls="products-menu"
      >
        Products
        <span aria-hidden="true">â–¼</span>
      </button>
      <ul id="products-menu" class="dropdown" hidden>
        <li><a href="/products/software">Software</a></li>
        <li><a href="/products/hardware">Hardware</a></li>
        <li><a href="/products/services">Services</a></li>
      </ul>
    </li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

Key ARIA Attributes

aria-expanded: Indicates dropdown state (true/false). Update when toggling.

aria-haspopup: Indicates element triggers a menu. Use "true" or "menu".

aria-controls: Points to the controlled menu's ID.

Button vs. Link for Dropdowns

Use `<button>` when: The element only toggles the dropdown (doesn't navigate).

Use `<a>` with additional markup when: The element both navigates and has a dropdown.

For links with dropdowns, separate the link from the toggle:

<li class="has-dropdown">
  <a href="/products">Products</a>
  <button
    aria-expanded="false"
    aria-label="Products submenu"
    aria-controls="products-menu"
  >
    <span aria-hidden="true">â–¼</span>
  </button>
  <ul id="products-menu" hidden>
    <!-- submenu items -->
  </ul>
</li>

JavaScript for Dropdowns

class DropdownMenu {
  constructor(button) {
    this.button = button;
    this.menu = document.getElementById(button.getAttribute('aria-controls'));

    this.init();
  }

  init() {
    this.button.addEventListener('click', () => this.toggle());
    this.button.addEventListener('keydown', (e) => this.handleButtonKey(e));
    this.menu.addEventListener('keydown', (e) => this.handleMenuKey(e));

    // Close when clicking outside
    document.addEventListener('click', (e) => {
      if (!this.button.contains(e.target) && !this.menu.contains(e.target)) {
        this.close();
      }
    });
  }

  toggle() {
    const isExpanded = this.button.getAttribute('aria-expanded') === 'true';
    if (isExpanded) {
      this.close();
    } else {
      this.open();
    }
  }

  open() {
    this.button.setAttribute('aria-expanded', 'true');
    this.menu.hidden = false;

    // Focus first menu item
    const firstItem = this.menu.querySelector('a');
    firstItem?.focus();
  }

  close() {
    this.button.setAttribute('aria-expanded', 'false');
    this.menu.hidden = true;
  }

  handleButtonKey(e) {
    switch (e.key) {
      case 'ArrowDown':
      case 'Enter':
      case ' ':
        e.preventDefault();
        this.open();
        break;
      case 'Escape':
        this.close();
        this.button.focus();
        break;
    }
  }

  handleMenuKey(e) {
    const items = [...this.menu.querySelectorAll('a')];
    const currentIndex = items.indexOf(document.activeElement);

    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        items[(currentIndex + 1) % items.length]?.focus();
        break;
      case 'ArrowUp':
        e.preventDefault();
        items[(currentIndex - 1 + items.length) % items.length]?.focus();
        break;
      case 'Escape':
        this.close();
        this.button.focus();
        break;
      case 'Tab':
        // Let Tab close menu naturally
        this.close();
        break;
    }
  }
}

// Initialize all dropdowns
document.querySelectorAll('[aria-haspopup="true"]').forEach(button => {
  new DropdownMenu(button);
});

Mega Menu Implementation

Structure for Complex Menus

Mega menus need more organized structure:

<nav aria-label="Main navigation">
  <ul class="nav-menu">
    <li class="mega-menu-item">
      <button
        aria-expanded="false"
        aria-haspopup="true"
        aria-controls="mega-products"
      >
        Products
      </button>
      <div id="mega-products" class="mega-menu" hidden>
        <div class="mega-menu-section">
          <h3 id="software-heading">Software</h3>
          <ul aria-labelledby="software-heading">
            <li><a href="/products/analytics">Analytics Platform</a></li>
            <li><a href="/products/crm">CRM Solution</a></li>
            <li><a href="/products/erp">ERP System</a></li>
          </ul>
        </div>
        <div class="mega-menu-section">
          <h3 id="hardware-heading">Hardware</h3>
          <ul aria-labelledby="hardware-heading">
            <li><a href="/products/servers">Servers</a></li>
            <li><a href="/products/networking">Networking</a></li>
            <li><a href="/products/storage">Storage</a></li>
          </ul>
        </div>
        <div class="mega-menu-section">
          <h3 id="services-heading">Services</h3>
          <ul aria-labelledby="services-heading">
            <li><a href="/products/consulting">Consulting</a></li>
            <li><a href="/products/support">Support</a></li>
            <li><a href="/products/training">Training</a></li>
          </ul>
        </div>
      </div>
    </li>
  </ul>
</nav>

Keyboard Navigation for Mega Menus

More complex menus need more sophisticated keyboard handling:

class MegaMenu {
  handleMenuKey(e) {
    const allItems = [...this.menu.querySelectorAll('a')];
    const sections = [...this.menu.querySelectorAll('.mega-menu-section')];
    const currentSection = document.activeElement.closest('.mega-menu-section');
    const sectionItems = [...currentSection.querySelectorAll('a')];
    const currentSectionIndex = sections.indexOf(currentSection);
    const currentItemIndex = sectionItems.indexOf(document.activeElement);

    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        // Move down within section
        sectionItems[(currentItemIndex + 1) % sectionItems.length]?.focus();
        break;

      case 'ArrowUp':
        e.preventDefault();
        // Move up within section
        sectionItems[(currentItemIndex - 1 + sectionItems.length) % sectionItems.length]?.focus();
        break;

      case 'ArrowRight':
        e.preventDefault();
        // Move to next section
        const nextSection = sections[(currentSectionIndex + 1) % sections.length];
        nextSection.querySelector('a')?.focus();
        break;

      case 'ArrowLeft':
        e.preventDefault();
        // Move to previous section
        const prevSection = sections[(currentSectionIndex - 1 + sections.length) % sections.length];
        prevSection.querySelector('a')?.focus();
        break;

      case 'Home':
        e.preventDefault();
        allItems[0]?.focus();
        break;

      case 'End':
        e.preventDefault();
        allItems[allItems.length - 1]?.focus();
        break;

      case 'Escape':
        this.close();
        this.button.focus();
        break;
    }
  }
}

Mobile Navigation

Hamburger Menu Accessibility

<button
  id="mobile-menu-toggle"
  aria-expanded="false"
  aria-controls="mobile-menu"
  aria-label="Menu"
>
  <span class="hamburger-icon" aria-hidden="true"></span>
</button>

<nav id="mobile-menu" aria-label="Main navigation" hidden>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Mobile Menu JavaScript

class MobileMenu {
  constructor() {
    this.toggle = document.getElementById('mobile-menu-toggle');
    this.menu = document.getElementById('mobile-menu');

    this.init();
  }

  init() {
    this.toggle.addEventListener('click', () => this.toggleMenu());

    // Close on Escape
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && this.isOpen()) {
        this.close();
        this.toggle.focus();
      }
    });
  }

  isOpen() {
    return this.toggle.getAttribute('aria-expanded') === 'true';
  }

  toggleMenu() {
    if (this.isOpen()) {
      this.close();
    } else {
      this.open();
    }
  }

  open() {
    this.toggle.setAttribute('aria-expanded', 'true');
    this.menu.hidden = false;

    // Focus first link
    this.menu.querySelector('a')?.focus();
  }

  close() {
    this.toggle.setAttribute('aria-expanded', 'false');
    this.menu.hidden = true;
  }
}

Touch Target Sizes

Mobile navigation needs adequate touch targets:

.nav-menu a,
.nav-menu button {
  min-height: 44px;
  min-width: 44px;
  padding: 12px 16px;
  display: flex;
  align-items: center;
}

WCAG 2.5.8 Target Size requires 24x24px minimum; 44px is recommended.

Focus Indicators

Visible Focus for All Menu Items

.nav-menu a:focus,
.nav-menu button:focus {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

/* Focus-visible for better UX */
.nav-menu a:focus:not(:focus-visible),
.nav-menu button:focus:not(:focus-visible) {
  outline: none;
}

.nav-menu a:focus-visible,
.nav-menu button:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

Highlight Current Position

Help users understand where they are in nested menus:

.nav-menu li:has(> button[aria-expanded="true"]) > button {
  background-color: #f0f0f0;
}

.dropdown a:focus,
.mega-menu a:focus {
  background-color: #e0e0e0;
  outline: none; /* Background change provides indication */
}

Testing Navigation Accessibility

Keyboard Testing Checklist

  • [ ] Can Tab to all navigation items
  • [ ] Can open dropdowns with Enter/Space
  • [ ] Arrow keys navigate within open menus
  • [ ] Escape closes menus and returns focus
  • [ ] Tab moves focus out of menu appropriately
  • [ ] Focus indicators visible throughout
  • [ ] Focus order follows visual order

Screen Reader Testing

  • [ ] Navigation landmark announced
  • [ ] Menu structure (list, items) announced
  • [ ] aria-expanded state announced for dropdowns
  • [ ] Submenu items announced within context
  • [ ] Current page indicated (aria-current)

Automated Testing

Automated tools check:

  • Proper ARIA attributes
  • Focus indicator presence
  • Color contrast
  • Landmark presence

They miss:

  • Keyboard navigation behavior
  • Focus management correctness
  • Screen reader experience

Common Navigation Mistakes

Mistake 1: Hover-Only Dropdowns

Problem: Menus only open on mouse hover—keyboard users can't access submenus.

Fix: Add click/keyboard activation alongside hover.

Mistake 2: Missing aria-expanded

Problem: Screen readers don't know dropdown state.

Fix: Toggle aria-expanded on the trigger element when opening/closing.

Mistake 3: Focus Disappears in Dropdowns

Problem: When dropdown closes, focus goes to body.

Fix: Return focus to the trigger button when closing.

Mistake 4: No Escape Key Support

Problem: Keyboard users can't close menus easily.

Fix: Add Escape key handler to close menus.

Mistake 5: Missing Navigation Landmark

Problem: Screen reader users can't find navigation.

Fix: Use <nav> element with descriptive aria-label.

FAQ Section

Q: Should dropdown menus open on hover or click?

A: Both can work accessibly. Click/keyboard activation is more reliable across devices. If using hover, also support keyboard activation (Enter/Space) and click for touch devices. Never hover-only.

Q: How do I handle submenus within submenus?

A: Multi-level menus become complex. Each level needs proper aria-expanded, keyboard navigation, and focus management. Consider whether information architecture can be simplified instead of building deeply nested menus.

Q: Should Tab close dropdown menus?

A: Typically yes—Tab should move focus to the next item after the menu trigger, implicitly closing the menu. This matches user expectations from native menu implementations.

Q: What's the difference between menu role and navigation with lists?

A: The ARIA menu role is for application menus (like OS menus) with specific keyboard expectations. Navigation menus are better served by <nav> with list structure. Don't add role="menu" to site navigation.

Q: How do I make mega menus accessible on mobile?

A: Mega menus typically need different treatment on mobile. Consider: expandable accordion pattern, separate navigation page, or simplified menu structure. Don't try to replicate desktop mega menu on small screens.

Building Accessible Navigation

Accessible navigation follows established patterns:

  • Semantic HTML with nav and lists
  • Keyboard activation and arrow navigation
  • Proper ARIA for expandable sections
  • Visible focus throughout
  • Escape to close, focus returns to trigger

Navigation is the foundation of site usability. Getting it right ensures everyone can find their way around.

Ready to verify your navigation accessibility? Get a free accessibility scan to identify issues with your menus and other components.


Related Articles:


This piece was developed through AI-assisted research and human editorial oversight. As specialists in source code accessibility remediation, we aim to provide actionable insights—but accessibility compliance decisions should involve qualified legal and technical professionals.

Stay informed

Accessibility insights delivered
straight to your inbox.

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo