Search topics
Dropdown parent links that open the submenu, not the page
One link both goes to a section page and opens its submenu, so a script decides what Enter does, and nothing says whether the list or the page will open.
event.preventDefault(); The problem
Keyboard users can't rely on a menu link to take them to its page, and screen reader users can't tell whether Enter will open a list or leave the page.
A furniture shop's header has three items: Home, Products and About. Products is a link to the shop's overview page, and resting the mouse on it drops down a list: Lamps, Chairs and Rugs. A mouse user moves onto the list to pick a category, or clicks Products to see everything. Both work, because the pointer can rest on the link without pressing it.
A keyboard has no resting. Tab to Products and press Enter, and the site has to choose between the list and the page. One common build chooses both, in turn: the first Enter opens the list and keeps you on the page, and the second follows the link. A screen reader says "Products, link" before the first press and after it, so its user hears nothing happen, presses again, and leaves for the Products page without ever meeting Lamps.
Another common build was written to make the menu work by keyboard: Enter now opens and closes the list, and the link says "collapsed" and "expanded". The overview page has gone missing on the way. A mouse click on Products still goes there; no key does. With a screen reader running, it can go the other way: in browse mode the screen reader often clicks the link itself rather than passing Enter to the page, so its user lands on the Products page and never sees the list.
A link is announced as a link because pressing it goes somewhere. Here one link has two jobs, going to its page and opening its list, and only a pointer can ask for each on its own: resting on the link for the list, a click for the page. The scripts in these builds decide which job a press gets, and the markup says nothing about the decision.
WCAG 2.1.1 Keyboard asks that everything the page offers can be done from a keyboard. The second build fails it: its key handler cancels Enter on Products and opens the list, while a mouse click still follows the link, so the overview page is one click away for a mouse and out of reach for a keyboard. The first build passes 2.1.1, because a second Enter does reach the page.
4.1.2 Name, Role, Value asks that a control's role and state can be read, and that a change to them is reported. In the first build, Products opens a list, so it has a state, open or closed, and nothing exposes it: the screen reader hears "Products, link" whether the list is open or not. In the second, aria-expanded exposes the state, but the role is still link, and the link no longer does what a link does on Enter. "Products, link, collapsed" tells its user that something expands, not that Enter will never take them to the page. A screen reader in browse mode usually turns Enter into a click on the link, which runs no key handler, so what happens depends on whether a screen reader is running.
What a tap does on a phone depends on the browser as much as the script: some show the list on the first tap and follow the link on the second, some follow it at once. So the same header can behave three ways for three visitors, and none of them is told which.
3 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 (affected)
- Seizures (not affected)
- Limited manipulation Keyboard and switch users press Enter on Products to go to the Products page. In the first build the list opens instead, with nothing to say it would, and only a second press leaves. In the second, no key ever reaches the page that a mouse click opens.
- Without vision Screen reader users hear a link and expect a new page. In the first build, Enter seems to do nothing and the next Enter leaves. In the second, they hear "collapsed" on a link, and in browse mode their screen reader may click the link rather than send Enter, so they land on the Products page and the list never opens for them.
- Limited language, cognitive and learning abilities Nothing on screen says whether pressing Products opens a list or a page, and the same press does one thing the first time and another the second, which is hard to learn and easy to lose track of.
A scanner reads <a href="/products">, a valid link with a name, and aria-expanded is allowed on a link, so nothing is flagged. It doesn't press Enter, so it never finds out that the link goes nowhere, or goes somewhere only the second time. Some published advice on making a dropdown keyboard accessible says to let Enter open the submenu without following the link, and an AI coding tool asked the same thing can write exactly the second build: a key handler on the link that cancels Enter and opens the list, with aria-expanded or aria-haspopup added. It passes the scan, and it moves the failure from the list to the page.
The audit presses Enter on each top-level link that holds a submenu and has no button beside it, with the link's navigation held back so the page stays put. Where Enter opens the list and never follows the link while a mouse click does, it reports that as a finding. Where Enter opens the list the first time and follows the link the second, or no press follows it at all, it sends a review lead, since a person has to judge what the page says and whether the overview page is linked elsewhere. Tabbing through the header with the mouse aside, and again with a screen reader in browse mode, is still how to confirm either.
Try it
Using only the keyboard, go to the Products page, then to Lamps.
Broken 1: press Enter on Products. The list opens, but the panel still says only "Products, link". Press Enter again and you leave for the Products page, with nothing to say you would.
Broken 2: Enter on Products opens and closes the list, and never reaches the Products page. Click Products with the mouse, and you get there at once.
Apply a fix. Enter on a link always goes to its page, and the list opens from a button that says whether it is open.
Broken Can you fix this?
Home
Screen reader says:
(Tab to the menu)
The fix
Give the page a link and the list a button of its own.
The first press opens the list, the second follows the link
<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>What is wrong2 points
A mouse opens the list on hover, so its click always follows the link. Enter on a link fires the same click, and that is where it goes wrong.
- The first Enter on Products is cancelled, and the list opens in its place.
- Products is announced as "Products, link" before and after, so nothing says the list opened, or that the next Enter will leave the page.
The link, and a button beside it
<nav aria-label="Main"> <ul class="menu"> <li> <a href="/">Home</a> </li> <li class="has-sub"> <a href="/products">Products</a> <button type="button" aria-expanded="false" aria-controls="sub-products" aria-label="Products pages">▾</button> <ul id="sub-products" tabindex="-1" hidden> <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>How it is fixed6 steps
- Leave Products as a plain link with no handler, so Enter, a click and a tap all go to the Products page, the first time.
- Put a native button straight after it, drawn as the chevron, which opens and closes the list with Enter, Space or a click.
- Name the button "Products pages", so a screen reader and voice control tell it apart from the link, and keep it at least 24 by 24 pixels.
- Give it
aria-expanded, kept in step withhidden, so it is announced as "Products pages, button, collapsed", then "expanded", and tie it to the list witharia-controls. - Close the list on Escape and put focus back on the button, and close it when focus leaves the item, without moving focus.
- Put
tabindex="-1"on the list, so a click inside it keeps focus within the item and doesn't count as leaving.
This fixes both Befores and passes 2.1.1 and 4.1.2: each control does one job and says which. It is the shape of the W3C's disclosure navigation example with top-level links, and of the WordPress Navigation block's submenus when they open on hover with their arrow shown.
Why this fix: Use native HTML first
Both pass. Keep the link and add a button when the section page matters on its own, as a shop's category or a company's About page usually does: it stays one click, tap or Enter away for everyone, and it is how the W3C example and the WordPress Navigation block draw it. It costs a second, smaller target per item, which needs its own name and enough size. Turn the item into a button when its page is only an index of what the list holds: there is one control per item, one Tab stop and one large target, and the overview page sits at the top of the list.
The button needs a name of its own. Don't give it the link's name: "Products, link" followed by "Products, button" sounds like the same thing twice, and a voice control user saying "click Products" gets a choice of two. "Products pages", or "More Products pages" in the form of the W3C example's "More About pages", says what opens. The chevron is drawn in the button, not the link, so it is the target for the list; a chevron drawn inside the link's text invites people to press it and get the page.
Other parts of a real menu each need their own answer.
- Hover: you may keep opening the list on hover for mouse users. Set the state through the same
setOpen, soaria-expandedstays true while the list shows, and meet 1.4.13: the list stays while the pointer moves onto it and closes on Escape, as tooltips that vanish on approach explains. - Key handlers: don't open the list from a
keydownhandler on the link. A screen reader in browse mode often sends a click rather than the key, so the handler runs for some users and not others. aria-haspopup: leave it off the link and the button. It announces a menu, and this list is a set of links, not a menu.- Target size: the chevron button is small by design; at under 24 by 24 pixels it needs space around it, as tap targets too small and too close together explains.
- Mobile: the same link and button work in a narrow-screen menu, where the list stacks and pushes the next item down. Don't switch to a first-tap-opens rule there.
Design ideas
One item can lead to its page and open its list.
Four ways to draw Products. Put the mouse aside and Tab through each: Enter on a word goes to its page, and Enter on a chevron or + opens the list. Three keep both within reach of the keyboard; in the last, only a pointer opens the list.
In the design file
- Decide whether a press on the word Products goes to its page or opens its list, never both.
- If the word goes to the page, draw the chevron or + as a target of its own, set apart by a line or a gap.
- If the word opens the list, put the Products page in the list, first and easy to spot.
- Name the chevron in the design file, such as “Products pages”, so it differs from the link beside it.
- Draw the open state too, with the chevron turned over or the + turned to −.
Passes
- One pill, split by a line Products and its chevron share one outlined pill, split by a 1px line, so the item reads as one thing with two ends: the word goes to the page, and the chevron opens the list and turns over. Each half fills on its own hover and takes its own focus ring; the ink is #1a1a1a on white, 17.40:1.
Rule of thumb: Use native HTML first
- A panel that leads with the overview Products only opens the panel, and the page it used to open becomes the panel’s first and largest link, a tinted card reading “All products”, #1a1a1a on #f5f2ec at 15.58:1. The header keeps one Tab stop, and the overview is one press further, where nobody misses it.
Rule of thumb: Use native HTML first
- Phone menu: the row and its own + In the phone menu, the Products row splits in two: the words go to the page, and a square + at the right end, set apart by a line, opens Lamps, Chairs and Rugs beneath it and turns to −. The first tap on the words always leaves, so nobody has to guess.
Rule of thumb: Use native HTML first
Fails
- Phone menu: a + drawn inside the linkWCAG 2.1.1 It looks like the row above, but the whole row is one link, and the + inside it opens the list only when a pointer lands on it. Tab stops once on Products, Enter goes to the page, and Lamps, Chairs and Rugs never open from the keyboard.
Rule of thumb: Make a gesture a shortcut, never the only way
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Touch, Zoom
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.
How screen readers handle aria-expanded
Limits
This guide covers a top-level item that is a link and opens its submenu as well. A submenu that opens only on hover, with no script and no button, and a button that never says whether its list is open, are covered in hover-only dropdowns, or ones that never say they are open. Application menus of commands follow the ARIA menu pattern instead. The scripts are minimal sketches 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-hybrid (opens in a new tab)
- ARIA APG: disclosure-navigation (opens in a new tab)
- WAI Tutorial: menus/flyout (opens in a new tab)
- webaim.org: mail_message (opens in a new tab)
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.