Accessible Drop-Down Menus in 2026 Without a Framework: Popover API, Keyboard Support, and Real-World Pitfalls
Most dropdown navigation menus on the web are broken. Not visually—they look fine, they animate nicely, they respond to mouse hover. They are broken for the roughly fifteen percent of users who navigate by keyboard, the screen reader users who cannot see the visual hover state, and the mobile users whose fingers are not precise enough to maintain hover on a nested submenu. In 2026, with the Popover API available in every major browser and ARIA patterns well-documented, there is no reason to ship a dropdown menu that only works for mouse users. Yet the majority of custom-built navigation menus still fail basic keyboard and assistive technology tests.
This guide covers how to build dropdown menus that genuinely work: keyboard navigable, screen reader compatible, touch-friendly, and functional without a JavaScript framework. If you have built navigation using a desktop website editor and want to add interactive dropdowns that do not exclude anyone, this is the practical walkthrough.
Why Most Dropdown Menus Fail
The standard broken dropdown works like this: a <div> with display: none is toggled to display: block on :hover of the parent <li>. Mouse users see the submenu appear. Keyboard users pressing Tab skip right past it because the hidden content is not focusable and no interaction reveals it. Screen reader users do not know the submenu exists because there are no ARIA attributes signaling its presence. Mobile users get inconsistent behavior because :hover does not map cleanly to touch.
The failures are specific and predictable:
- No keyboard trigger. Hover is not a keyboard event. If the submenu only appears on hover, keyboard users cannot open it.
- No escape handling. Once a user tabs into a submenu, pressing Escape should close it and return focus to the parent trigger. Most custom menus do not implement this.
- No arrow key navigation. Users expect Up/Down arrows to move through menu items and Left/Right to navigate between top-level items. Without this, keyboard users must Tab through every single item linearly.
- No ARIA roles. Without
role="menu",role="menuitem",aria-expanded, andaria-haspopup, screen readers cannot convey the structure of the navigation. The user hears a flat list of links with no indication of hierarchy. - Focus escapes. Tab moves focus out of the submenu and into the next section of the page, leaving the submenu visually open but orphaned.
These are not edge cases. They are the default behavior of every CSS-only dropdown menu and most JavaScript dropdown implementations that were not specifically designed for accessibility.
The Popover API: What It Gives You for Free
The Popover API, fully supported across Chrome, Edge, Firefox, and Safari as of 2026, provides a browser-native mechanism for showing and hiding content with built-in accessibility features. When you use popover on an element and popovertarget on a button, the browser handles several things automatically:
- Toggle visibility without JavaScript.
- Light dismiss. Clicking outside the popover or pressing Escape closes it.
- Top-layer rendering. The popover sits above other content without z-index hacking.
- Focus management baseline. The browser moves focus to the popover when appropriate.
For dropdown menus, this means you get the toggle behavior and escape-to-close for free. You still need to add keyboard navigation and ARIA semantics, but the foundation is dramatically better than a CSS-only or custom JavaScript approach.
Basic Popover Dropdown Structure
<nav aria-label="Main navigation">
<ul role="menubar">
<li role="none">
<button
role="menuitem"
aria-haspopup="true"
aria-expanded="false"
popovertarget="services-menu"
>
Services
</button>
<ul id="services-menu" role="menu" popover>
<li role="none">
<a role="menuitem" href="/web-design/">Web Design</a>
</li>
<li role="none">
<a role="menuitem" href="/hosting/">Hosting</a>
</li>
<li role="none">
<a role="menuitem" href="/support/">Support</a>
</li>
</ul>
</li>
<li role="none">
<a role="menuitem" href="/about/">About</a>
</li>
</ul>
</nav>
The popovertarget attribute links the button to the submenu by ID. Clicking the button toggles the submenu’s visibility. Pressing Escape while the submenu is open closes it. No JavaScript required for that basic behavior.
Updating aria-expanded
The Popover API toggles visibility but does not automatically update aria-expanded. You need a small script:
document.querySelectorAll('[popovertarget]').forEach(trigger => {
const target = document.getElementById(trigger.getAttribute('popovertarget'));
target.addEventListener('toggle', (event) => {
trigger.setAttribute('aria-expanded', event.newState === 'open');
});
});
The toggle event fires whenever a popover opens or closes, regardless of whether the toggle was initiated by the button click, an Escape press, or a light-dismiss click. This keeps aria-expanded synchronized automatically.
Adding Keyboard Navigation
The Popover API handles Escape. It does not handle arrow keys. For a menu to feel right to keyboard users, you need arrow key navigation within the submenu and between top-level items.
Arrow Key Behavior
The expected keyboard pattern for a horizontal menubar with vertical dropdowns:
| Key | Behavior |
|---|---|
| Enter / Space | Open submenu, move focus to first item |
| Escape | Close submenu, return focus to trigger button |
| Down Arrow | Move to next item in submenu |
| Up Arrow | Move to previous item in submenu |
| Right Arrow | Move to next top-level menu item |
| Left Arrow | Move to previous top-level menu item |
| Home | Move to first item in current menu |
| End | Move to last item in current menu |
Implementation
function setupMenuKeyboard(menubar) {
const topItems = [...menubar.querySelectorAll(':scope > li > [role="menuitem"]')];
topItems.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
topItems[(index + 1) % topItems.length].focus();
break;
case 'ArrowLeft':
e.preventDefault();
topItems[(index - 1 + topItems.length) % topItems.length].focus();
break;
case 'ArrowDown':
if (item.getAttribute('aria-haspopup') === 'true') {
e.preventDefault();
const submenu = document.getElementById(
item.getAttribute('popovertarget')
);
submenu.showPopover();
const firstChild = submenu.querySelector('[role="menuitem"]');
if (firstChild) firstChild.focus();
}
break;
}
});
});
// Handle navigation within submenus
document.querySelectorAll('[role="menu"][popover]').forEach(submenu => {
const items = [...submenu.querySelectorAll('[role="menuitem"]')];
items.forEach((item, index) => {
item.setAttribute('tabindex', '-1');
item.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
items[(index + 1) % items.length].focus();
break;
case 'ArrowUp':
e.preventDefault();
items[(index - 1 + items.length) % items.length].focus();
break;
case 'Home':
e.preventDefault();
items[0].focus();
break;
case 'End':
e.preventDefault();
items[items.length - 1].focus();
break;
}
});
});
});
}
Set tabindex="-1" on submenu items so they are focusable programmatically but not in the normal Tab order. This prevents keyboard users from having to Tab through every submenu item when navigating the page linearly—they only enter the submenu when they explicitly open it.
Focus Management: The Part Everyone Gets Wrong
The most common focus management mistake in dropdown menus is failing to return focus to the trigger button when the submenu closes. The Popover API’s light-dismiss behavior closes the popover, but it does not automatically move focus anywhere useful. If the user pressed Escape, focus should return to the button that opened the menu. If the user clicked outside the menu, focus goes wherever they clicked, which is correct. The distinction matters.
document.querySelectorAll('[role="menu"][popover]').forEach(submenu => {
const triggerId = submenu.id;
const trigger = document.querySelector(`[popovertarget="${triggerId}"]`);
submenu.addEventListener('toggle', (event) => {
if (event.newState === 'closed') {
// Only restore focus if focus was inside the submenu
if (submenu.contains(document.activeElement) || document.activeElement === document.body) {
trigger.focus();
}
}
});
});
This checks whether focus was inside the submenu when it closed. If the user clicked somewhere else on the page, document.activeElement will be that other element, and you leave focus alone. If focus was inside the submenu (Escape was pressed) or on the document body (the browser cleared focus during the close), you return it to the trigger.
Styling the Open State
CSS for popover-based dropdowns is simpler than the old display: none approach because the popover’s visibility is managed by the browser. You style the open state and the positioning:
[role="menu"][popover] {
position: absolute;
inset: unset;
top: 100%;
left: 0;
margin: 0;
padding: 0.5rem 0;
border: 1px solid #ddd;
background: #fff;
list-style: none;
min-width: 200px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
[role="menu"][popover]:popover-open {
display: flex;
flex-direction: column;
}
[role="menuitem"]:focus-visible {
outline: 2px solid #0066cc;
outline-offset: -2px;
background: #f0f4ff;
}
The :popover-open pseudo-class lets you style the menu only when it is visible. The :focus-visible pseudo-class ensures the focus indicator appears for keyboard users but not for mouse clicks, matching the behavior users expect.
Important: do not set outline: none on menu items globally. The focus indicator is the keyboard user’s cursor. Removing it is the equivalent of hiding the mouse pointer for sighted users. If the default browser outline does not match your design, replace it with a visible custom style. Never remove it entirely.
Common Pitfalls and How to Fix Them
Hover-Only Menus
A menu that only responds to :hover excludes keyboard users, screen reader users, and many touch users. The fix is to use a <button> with popovertarget as the trigger. Buttons are natively focusable and activatable by keyboard. You can still add hover behavior as an enhancement:
li:hover > [popover] {
/* Optional: also show on hover for mouse users */
}
But the button trigger must remain the primary interaction method. Hover is a progressive enhancement, not the baseline.
Focus Traps in Submenus
A focus trap occurs when a keyboard user cannot Tab out of the submenu. This happens when every item in the submenu has tabindex="0" and the submenu intercepts Tab events without providing an exit path. The fix is tabindex="-1" on submenu items (removing them from the Tab order) combined with proper Escape handling that closes the submenu and returns focus to the trigger.
Missing Escape Handling
The Popover API handles Escape automatically for the light-dismiss behavior, but if you have built a custom dropdown without the Popover API, you need to add Escape handling yourself. Every interactive component that opens a layer above the page must close on Escape. This is non-negotiable for accessibility compliance.
Nested Submenus
Deeply nested submenus (sub-sub-menus) are problematic on every dimension: they are hard to navigate by keyboard, confusing for screen readers, and nearly impossible to use on touch devices. If your navigation requires more than two levels, reconsider your information architecture. A mega-menu pattern with grouped links in a single panel is almost always more usable than nested dropdown layers.
Submenus That Disappear Too Quickly
CSS hover-based submenus disappear the instant the mouse leaves the trigger area. If there is even a pixel gap between the trigger and the submenu, or if the user’s mouse path arcs slightly outside the menu boundary, the submenu vanishes. The Popover API avoids this because the menu is toggled by a click, not maintained by continuous hover. This alone is a significant usability improvement.
How DFM2HTML Handles Navigation Menus
DFM2HTML’s JavaScript menu system generates navigation structures with keyboard support built in. When you design a site with DFM2HTML and add a navigation menu through the editor, the exported HTML includes the JavaScript necessary for keyboard-accessible dropdown behavior. The feature overview covers the specific navigation components available.
For sites that need the Popover API approach described in this guide, the workflow is straightforward: design your page layout and static content in DFM2HTML, export the HTML, and then add the popover attributes and the keyboard navigation script to the exported files. The navigation structure DFM2HTML generates—nested unordered lists inside a <nav> element—is the correct semantic starting point for both ARIA menu roles and Popover API integration.
The tutorials section covers related navigation topics including tooltips, modals, and page transition patterns that share the same accessibility requirements: keyboard operability, screen reader semantics, and focus management.
Testing Your Dropdown Menu
Building the menu is half the work. Testing it is the other half, and skipping it is how accessible-looking code ships with inaccessible behavior.
Keyboard-only test. Unplug your mouse (or just do not touch it). Navigate to your menu using Tab. Open a submenu with Enter or Space. Move through items with arrow keys. Close with Escape. Verify focus returns to the trigger. If any step fails, the menu is not keyboard accessible.
Screen reader test. Enable Narrator on Windows (Win + Ctrl + Enter) or download NVDA (free). Navigate to the menu. Verify the screen reader announces the menu structure: “Services, menu button, collapsed” when focused on the trigger, “Services menu, three items” when opened. Each item should be announced with its role. If the screen reader reads a flat list of links with no structural context, your ARIA attributes are missing or incorrect.
Touch test. Open your page on a phone or use browser DevTools device emulation. Tap the menu trigger. Verify the submenu opens. Tap a submenu item. Verify it navigates. Tap outside the menu. Verify it closes. Hover-dependent behavior will not work on touch; the tap-based Popover API interaction must carry the full experience.
Reduced motion test. If you added CSS transitions to the menu, verify they are suppressed when the user has prefers-reduced-motion: reduce set in their operating system:
@media (prefers-reduced-motion: reduce) {
[role="menu"][popover] {
transition: none;
}
}
What Accessible Navigation Actually Requires
The Web Content Accessibility Guidelines (WCAG) 2.2 at Level AA require that all interactive components be operable by keyboard (Success Criterion 2.1.1), that focus order be logical (2.4.3), and that focus be visible (2.4.7). A dropdown menu that fails any of these criteria fails WCAG compliance, regardless of how polished it looks visually.
The Popover API does not make a menu automatically accessible. It provides a foundation—toggle behavior, light dismiss, top-layer rendering—that eliminates the most common implementation bugs. The ARIA attributes, keyboard navigation, and focus management described in this guide are still your responsibility. But starting from a solid foundation means less custom code, fewer edge cases, and a menu that works for everyone who visits your site.
Build the menu. Test it without a mouse. Fix what breaks. That is the entire process, and in 2026 there is no technical barrier to getting it right.