Search topics
Dropdown menus that open only on hover
The submenu under a top-level item is shown by a :hover rule alone, so Tab skips every page in it.
.has-sub:hover > ul { display: block; } The problem
Keyboard and screen reader users can't reach the pages listed under a top-level menu item.
A furniture shop's header has three items: Home, Products and About. Move the mouse over Products and a list drops down under it: Lamps, Chairs and Rugs. Put the mouse aside and press Tab instead. Focus goes to Home, then Products, then About. The list never appears, and nothing on screen or in the screen reader suggests it exists. Enter on Products follows its link to an overview page, so a keyboard user can still reach the shop, but each category page is two or three clicks further away, or not linked from anywhere else at all.
The submenu is hidden with display: none and shown by a single rule, .has-sub:hover > ul. Hover is something only a pointer can do, so the links inside the list can't receive focus while it is hidden, and nothing a keyboard can do will show it. WCAG 2.1.1 Keyboard requires that everything the page offers can be done with a keyboard, and reaching those links is part of what it offers. The top-level item also gives no sign that it has a submenu, or whether it is open, which 4.1.2 Name, Role, Value asks of anything that works as a control.
Touch screens have no hover either. Some browsers open the list on the first tap and follow the link on the second, others follow the link straight away, so whether a phone user ever sees the list depends on the browser.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Limited manipulation Keyboard and switch users tab from Products straight to About, so the pages listed under Products can't be reached from the menu at all. Voice control users can say "click Products", but that follows the link; there is no command for hover.
- Without vision Screen reader users hear "Products, link" with nothing to say pages sit beneath it, and the hidden links are not in what they can read or tab to.
A scanner reads the markup: valid links inside a list inside a nav. It doesn't hover, so it never sees the submenu open, and it doesn't compare what a mouse can reach with what Tab can reach. Hidden links aren't reported, because hiding closed content is correct. AI coding tools asked to make the menu accessible often add role="menubar" and role="menuitem". That is not a fix: those roles promise the arrow-key behaviour of an application menu, which the code then has to supply, and on a site's navigation they make screen readers switch modes and announce a menu that isn't one. Others add aria-haspopup="true" to a link, which announces a popup that still can't be opened. Putting the mouse aside and tabbing through the header is the only way to find this.
Try it
Reach Lamps using only Tab and Enter.
Hover over Products with a mouse to see the list, then try the keyboard. With the list open, try Escape, and keep tabbing past About: does the list cover the next link?
Broken Can you fix this?
This week's offer: Spring sale
Screen reader says:
(Tab to the menu)A recreated demo. The broken version is intentionally inaccessible.
The fix
Give every submenu a way to open from the keyboard, and say whether it is open.
- 1Every submenu opens without a mouse: from a button, a summary or keyboard focus.
- 2A control that opens a submenu says whether it is open.
- 3An open submenu that covers the page closes with Escape and never hides the control that has focus.
<nav aria-label="Main"> <ul class="menu"> <li><a href="/">Home</a></li> <li class="has-sub"> <a href="/products">Products</a> <ul> <li><a href="/products/lamps">Lamps</a></li> <li><a href="/products/chairs">Chairs</a></li> <li><a href="/products/rugs">Rugs</a></li> </ul> </li> <li><a href="/about">About</a></li> </ul></nav>A button that opens the submenu
<nav aria-label="Main"> <ul class="menu"> <li><a href="/">Home</a></li> <li class="has-sub"> <button type="button" aria-expanded="false" aria-controls="sub-products">Products</button> <ul id="sub-products" tabindex="-1" hidden> <li><a href="/products">All products</a></li> <li><a href="/products/lamps">Lamps</a></li> <li><a href="/products/chairs">Chairs</a></li> <li><a href="/products/rugs">Rugs</a></li> </ul> </li> <li><a href="/about">About</a></li> </ul></nav>Why this fix: Use native HTML first
Tab goes from Products to About, because the three category links are display: none until a pointer rests on Products. Nothing in the markup says Products has a submenu.
A native button opens and closes with Enter and Space, and aria-expanded makes a screen reader say "Products, button, collapsed", then "expanded". The list follows the button in the markup, so Tab goes straight from the button into the open list, and hidden keeps the links out of the Tab order while it is closed. Escape closes the list and puts focus back on Products. Moving focus out of the item, with Tab, Shift + Tab or a click elsewhere, closes it without moving focus anywhere, so an open list never covers the next thing people tab to. tabindex="-1" on the list means a click inside it keeps focus within the item, so that click doesn't count as leaving. The button no longer goes to the overview page, so that page moves into the list as "All products".
Use the button. It passes on its own, with nothing left that depends on the page around it: it says whether the list is open, closes on Escape and when focus leaves, and adds a single Tab stop per submenu. details and summary need no script and are a sound choice where the open list pushes the page down, but over the page they stay open behind the user. Opening on :focus-within is the smallest change to an existing menu and stops the worst of the failure while the button is built, but it can't be closed with Escape and doesn't tell screen reader users the list is there.
A top-level item that is also a page needs a choice. Moving the page into the list as "All products", as in the snippets, keeps one control per item. If the item must stay a link, put the link and a separate small button side by side, and give the button a name such as "Products pages" so it doesn't read as a second "Products".
Other parts of a real menu each need their own answer.
- Hover: you may keep opening the list on hover for mouse users. Then it is content that appears on hover, which 1.4.13 asks to stay open while the pointer moves onto it and to close on Escape; hover content covers this.
- Arrow keys: optional for a site menu. The disclosure pattern works with Tab alone; if you add arrow keys between top-level buttons, keep Tab working as well.
aria-haspopup: leave it out. On a button it announces a menu, and this list is a set of links, not a menu.- Mobile: the same buttons work in a narrow-screen menu, where the lists stack and push content down. The "Menu" button that opens the whole panel follows the same pattern.
- Current page: when a category page is open, mark its link with
aria-current="page", as current-page explains.
Fixing this with an AI coding assistant? Get this guide as Markdown
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Zoom, Touch
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared header or menu component changes.
Limits
This is a starting pattern for a site's navigation menu, not for application menus with commands, which follow the ARIA menu pattern and its arrow keys. The script is a minimal sketch without animation, hover opening or nested levels. Test the complete journey, including your narrow-screen menu, with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: keyboard (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- ARIA APG: disclosure-navigation (opens in a new tab)
- WAI Tutorial: menus/flyout (opens in a new tab)
- WCAG Understanding: content-on-hover-or-focus (opens in a new tab)
- WCAG Understanding: focus-not-obscured-minimum (opens in a new tab)
- developer.mozilla.org: details (opens in a new tab)
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.