← Back to Accessalyze

By Genesis AI Services · April 21, 2026 · 9 min read · Navigation

How to Make a Dropdown Menu Accessible

The minimum requirements: (1) opens with Enter/Space on the toggle button, (2) arrow keys navigate items, (3) Escape closes it and returns focus, (4) aria-expanded reflects open/closed state, (5) visible focus indicator inside the menu.

Navigation dropdowns are one of the most commonly broken keyboard patterns. The failure mode: hover-only dropdowns that keyboard users can never reach. This guide covers both navigation dropdowns and selection dropdowns.

Pattern 1: Navigation Dropdown (Disclosure Widget)

The W3C's recommended pattern for site navigation dropdowns is the disclosure navigation menu. A button toggles a list of links. No complex ARIA combobox needed.

See how 321 websites scored →

View the 2026 Report

HTML

<nav aria-label="Main navigation">
  <ul>
    <li>
      <button type="button"
              aria-expanded="false"
              aria-controls="products-menu"
              id="products-btn">
        Products
        <span aria-hidden="true"> ▾</span>
      </button>
      <ul id="products-menu" hidden>
        <li><a href="/products/scanner">Accessibility Scanner</a></li>
        <li><a href="/products/ci-integration">CI Integration</a></li>
        <li><a href="/products/api">Public API</a></li>
      </ul>
    </li>
    <li><a href="/docs">Docs</a></li>
    <li><a href="/pricing">Pricing</a></li>
  </ul>
</nav>

JavaScript

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

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

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

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

  open() {
    this.button.setAttribute('aria-expanded', 'true');
    this.menu.removeAttribute('hidden');
    // Focus first menu item
    this.menu.querySelector('a, button').focus();
  }

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

  handleButtonKey(e) {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      this.open();
    }
    if (e.key === 'Escape') {
      this.close();
      this.button.focus();
    }
  }

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

    if (e.key === 'ArrowDown') {
      e.preventDefault();
      items[(idx + 1) % items.length].focus();
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      items[(idx - 1 + items.length) % items.length].focus();
    }
    if (e.key === 'Escape') {
      this.close();
      this.button.focus();
    }
    if (e.key === 'Tab') {
      this.close();
      // Let natural tab order proceed
    }
  }
}

document.querySelectorAll('[aria-controls][aria-expanded]').forEach(btn => {
  new DisclosureDropdown(btn);
});

Required Keyboard Interactions

KeyBehavior
Enter or Space on buttonToggle open/closed
Arrow Down (on button)Open and move focus to first item
Arrow Down (in menu)Move to next item (wrap to first at end)
Arrow Up (in menu)Move to previous item (wrap to last at top)
EscapeClose menu, return focus to toggle button
TabClose menu, move focus to next focusable element on page
Home (optional)Move to first item
End (optional)Move to last item

Pattern 2: Select Replacement (Listbox)

If you're building a custom styled select/combobox, the complexity increases significantly. For pure selection (no search), the W3C's listbox pattern applies. For searchable selects, use the combobox pattern.

Practical advice: use the native <select> element whenever possible. You can style it with CSS and it is fully accessible out of the box. Only build a custom widget if the design requirements genuinely cannot be met with a styled native <select>.

<!-- Native select — accessible and styleable -->
<label for="country">Country</label>
<select id="country" name="country">
  <option value="">Select a country</option>
  <option value="us">United States</option>
  <option value="uk">United Kingdom</option>
  <option value="ca">Canada</option>
</select>

CSS: Hover-Only = Keyboard Inaccessible

/* FAIL: only opens on hover — keyboard users can't trigger this */
.nav-item:hover .dropdown-menu {
  display: block;
}

/* PASS: combine hover AND focus-within for keyboard support */
.nav-item:hover .dropdown-menu,
.nav-item:focus-within .dropdown-menu {
  display: block;
}

/* But prefer the JS toggle approach for full control */

Check Your Navigation for Accessibility Issues

Accessalyze detects inaccessible interactive elements including navigation patterns.

Scan Your Navigation →

← Back to Accessalyze

Accessalyze - Free WCAG 2.1 scanner that writes the fix code for you | Product Hunt

See real website accessibility scores: Browse 244+ free accessibility audits →

Try it yourself

Enter your website URL to get a free accessibility score.

Check your website accessibility score free Scan Now →