- by Wouter Nordsiek
- 0 Comments
- Guide
- 30 September 2025
Accessible Dropdown Menus
What you’ll learn: Build dropdown menus that work for everyone, including keyboard users and screen reader users. This guide provides complete, tested code that you can copy and implement immediately.
Time to implement: 15-30 minutes
Skill level: Intermediate (HTML, CSS, basic JavaScript)
Why Dropdown Accessibility Matters
Dropdown menus are one of the most common UI patterns, but they’re often implemented incorrectly. Poor dropdown accessibility prevents users from:
- Navigating with keyboards (no mouse)
- Understanding menu structure with screen readers
- Accessing menu items on mobile devices
- Using the interface with motor impairments
Common mistake: Many developers focus only on visual appearance and miss the accessibility requirements. This excludes millions of users and can create legal compliance issues.
WCAG Requirements
- 2.1.1 Keyboard (Level A) – All functionality must be keyboard accessible
- 4.1.2 Name, Role, Value (Level A) – UI components must have accessible names and roles
- 2.4.3 Focus Order (Level A) – Focus must follow a logical sequence
Working Example
Try this: Use mouse, keyboard (Tab, Enter, Arrow keys, Escape), or screen reader to navigate:
Keyboard shortcuts:
- Tab: Focus the dropdown button
- Enter/Space: Open/close dropdown
- Arrow keys: Navigate menu items
- Escape: Close dropdown and return focus
- Enter: Activate focused menu item
Common Problems
✗ Inaccessible Implementation
Problems:
- No keyboard support (try Tab key!)
- No ARIA labels or semantic HTML
- Screen readers can’t understand structure
- Focus management missing
- Uses generic <div> instead of <button>
Step-by-Step Implementation
HTML Structure
Start with semantic HTML and proper ARIA attributes:
Complete HTML Structure
<ul role=”menu” hidden>…menu items…</ul>
<div class="dropdown">
<button class="dropdown-button"
aria-expanded="false"
aria-haspopup="menu"
id="settings-button">
Settings
<span class="dropdown-arrow">▼</span>
</button>
<ul class="dropdown-menu"
role="menu"
aria-labelledby="settings-button"
hidden>
<li role="none">
<a href="/profile" class="dropdown-item" role="menuitem" tabindex="-1">
Edit Profile
</a>
</li>
<li role="none">
<a href="/settings" class="dropdown-item" role="menuitem" tabindex="-1">
Account Settings
</a>
</li>
<li role="none">
<a href="/logout" class="dropdown-item" role="menuitem" tabindex="-1">
Sign Out
</a>
</li>
</ul>
</div>
Key ARIA attributes:
aria-expanded
: Tells screen readers if menu is open/closedaria-haspopup="menu"
: Indicates button opens a menurole="menu"
: Identifies the dropdown as a menurole="menuitem"
: Identifies individual menu optionsrole="none"
: Removes semantic meaning from <li> elements (menu items provide the semantics)aria-labelledby
: Links menu to its buttontabindex="-1"
: Removes menu items from tab order (JavaScript manages focus)
Screen Reader Announcements
With proper ARIA, screen readers will announce:
- Button: “Account Settings, button, has popup menu, collapsed”
- When opened: “Account Settings, button, has popup menu, expanded”
- Menu items: “Edit Profile, menu item, 1 of 4”
- Navigation: “Privacy Settings, menu item, 3 of 4”
CSS Styling
Style the dropdown with proper focus indicators:
CSS Styling with Focus Indicators
.dropdown-button:focus { outline: 3px solid #80bdff; }
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-button {
background: #007bff;
color: white;
border: none;
padding: 12px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.dropdown-button:hover {
background: #0056b3;
}
.dropdown-button:focus {
outline: 3px solid #80bdff;
outline-offset: 2px;
}
.dropdown-arrow {
transition: transform 0.2s ease;
font-size: 12px;
}
.dropdown-button[aria-expanded="true"] .dropdown-arrow {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
min-width: 180px;
list-style: none;
margin: 0;
padding: 0;
}
.dropdown-menu[hidden] {
display: none;
}
.dropdown-item {
display: block;
width: 100%;
padding: 12px 16px;
text-decoration: none;
color: #333;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.dropdown-item:hover,
.dropdown-item:focus {
background: #f8f9fa;
outline: none;
}
.dropdown-item:focus {
background: #e3f2fd;
}
JavaScript Functionality
Add keyboard support and focus management:
JavaScript with Keyboard Support
// + Keyboard navigation, focus management, outside clicks
function initDropdown(buttonSelector) {
const button = document.querySelector(buttonSelector);
const menu = document.querySelector(button.getAttribute('aria-controls') ||
button.id.replace('button', 'menu'));
if (!button || !menu) return;
// Toggle dropdown
button.addEventListener('click', function() {
const isOpen = button.getAttribute('aria-expanded') === 'true';
toggleDropdown(button, !isOpen);
});
// Keyboard navigation
button.addEventListener('keydown', function(e) {
switch(e.key) {
case 'Enter':
case ' ':
e.preventDefault();
const isOpen = button.getAttribute('aria-expanded') === 'true';
toggleDropdownExample(button, !isOpen);
if (!isOpen) {
// Focus first item when opening
setTimeout(() => {
const firstItem = menu.querySelector('[role="menuitem"]');
if (firstItem) firstItem.focus();
}, 50);
}
break;
case 'ArrowDown':
e.preventDefault();
if (button.getAttribute('aria-expanded') === 'false') {
toggleDropdown(button, true);
setTimeout(() => {
const firstItem = menu.querySelector('[role="menuitem"]');
if (firstItem) firstItem.focus();
}, 50);
}
break;
case 'Escape':
toggleDropdownExample(button, false);
button.focus();
break;
}
});
// Menu item navigation
menu.addEventListener('keydown', function(e) {
const items = menu.querySelectorAll('[role="menuitem"]');
const currentIndex = Array.from(items).indexOf(document.activeElement);
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
const nextIndex = (currentIndex + 1) % items.length;
items[nextIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
items[prevIndex].focus();
break;
case 'Escape':
e.preventDefault();
toggleDropdownExample(button, false);
button.focus();
break;
case 'Enter':
e.preventDefault();
if (document.activeElement.href) {
window.location.href = document.activeElement.href;
} else {
document.activeElement.click();
}
break;
}
});
// Close on outside click
document.addEventListener('click', function(e) {
if (!button.contains(e.target) && !menu.contains(e.target)) {
toggleDropdownExample(button, false);
}
});
}
function toggleDropdownExample(button, open) {
const menu = document.querySelector('#' + button.getAttribute('aria-controls') ||
button.id.replace('button', 'menu'));
button.setAttribute('aria-expanded', open);
menu.hidden = !open;
if (!open) {
// Return focus to button when closing
button.focus();
}
}
// Initialize dropdown (disabled - using initializeDropdownDemo instead)
// document.addEventListener('DOMContentLoaded', function() {
// initDropdown('#settings-button');
// });
Testing Your Implementation
Verify accessibility with these tests:
Manual Testing Checklist
- Tab to dropdown button and verify focus indicator
- Press Enter/Space to open dropdown
- Use arrow keys to navigate menu items
- Press Escape to close and return focus to button
- Click outside dropdown to close it
- Test with screen reader (enable VoiceOver/NVDA)
- Verify dropdown works on mobile touch devices
- Check dropdown doesn’t break at 200% zoom
Advanced Patterns
Dropdown with Dividers
Menu with Visual Separators
<ul class="dropdown-menu" role="menu">
<li role="none">
<a href="#" class="dropdown-item" role="menuitem" tabindex="-1">Edit Profile</a>
</li>
<li role="none">
<a href="#" class="dropdown-item" role="menuitem" tabindex="-1">Settings</a>
</li>
<li role="separator" class="dropdown-divider"></li>
<li role="none">
<a href="#" class="dropdown-item" role="menuitem" tabindex="-1">Sign Out</a>
</li>
</ul>
Dropdown with Icons
Menu Items with Accessible Icons
<li role="none">
<a href="#" class="dropdown-item" role="menuitem" tabindex="-1">
<span class="icon" aria-hidden="true">👤</span>
Edit Profile
</a>
</li>
Common Mistakes to Avoid
✗ Common Implementation Mistakes
<!-- Missing ARIA attributes -->
<div class="dropdown" onclick="showMenu()">Menu</div>
<!-- Div instead of button -->
<div class="dropdown-button">Settings</div>
<!-- Missing keyboard support -->
<button onclick="toggleMenu()">Menu</button>
<!-- No focus management -->
function openMenu() {
menu.style.display = 'block';
// Missing: focus first item
}
<!-- Missing tabindex on menu items -->
<a href="#" role="menuitem">Profile</a>
// Should be: <a href="#" role="menuitem" tabindex="-1">Profile</a>
✓ Best Practices Summary
- Always use
<button>
for dropdown triggers - Include all required ARIA attributes (
aria-expanded
,aria-haspopup="menu"
) - Add
tabindex="-1"
to all menu items - Implement full keyboard navigation
- Manage focus properly (open/close/escape)
- Test with real screen readers
- Ensure touch device compatibility
Browser Support
This implementation works in all modern browsers including:
- Chrome 70+
- Firefox 65+
- Safari 12+
- Edge 79+
- iOS Safari 12+
- Android Chrome 70+