Close
Build Accessible Dropdown Menus: Complete Developer Guide with WCAG Examples

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

WCAG 2.2 Guidelines:

Working Example

Interactive Demo: Accessible Dropdown

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

Menu ▼
Option 1
Option 2
Option 3

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>

✓ Accessible Implementation

Features:

  • Full keyboard navigation (Tab, Enter, Arrows)
  • Proper ARIA attributes
  • Screen reader announcements
  • Focus management
  • Escape key support

Step-by-Step Implementation

1

HTML Structure

Start with semantic HTML and proper ARIA attributes:

Complete HTML Structure

<button aria-expanded=”false” aria-haspopup=”menu”>Settings</button>
<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/closed
  • aria-haspopup="menu": Indicates button opens a menu
  • role="menu": Identifies the dropdown as a menu
  • role="menuitem": Identifies individual menu options
  • role="none": Removes semantic meaning from <li> elements (menu items provide the semantics)
  • aria-labelledby: Links menu to its button
  • tabindex="-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”
2

CSS Styling

Style the dropdown with proper focus indicators:

CSS Styling with Focus Indicators

.dropdown-button { background: #007bff; padding: 12px 16px; }
.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;
}
3

JavaScript Functionality

Add keyboard support and focus management:

JavaScript with Keyboard Support

button.addEventListener(‘click’, () => toggleDropdown());
// + 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');
// });
4

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

<li role=”separator” class=”dropdown-divider”></li>
<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

<span class=”icon” aria-hidden=”true”>👤</span> Edit Profile
<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

<div onclick=”showMenu()”>Menu</div> // Missing ARIA, keyboard support
<!-- 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+

Related Resources

Leave a Reply