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.
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<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>
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);
});
| Key | Behavior |
|---|---|
| Enter or Space on button | Toggle 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) |
| Escape | Close menu, return focus to toggle button |
| Tab | Close menu, move focus to next focusable element on page |
| Home (optional) | Move to first item |
| End (optional) | Move to last item |
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>
/* 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 */
Accessalyze detects inaccessible interactive elements including navigation patterns.
Scan Your Navigation →See real website accessibility scores: Browse 244+ free accessibility audits →
Try it yourself
Enter your website URL to get a free accessibility score.