# Dropdown menus that open only on hover

Source: https://easeweb.dev/learn/nav-menus
Topics: Menus and navigation > Navigation menus open with a keyboard (WCAG 2.1.1, 4.1.2)
The fix: Either turn each top-level item into a button that opens its submenu, says whether it is open with aria-expanded, and closes on Escape and when focus leaves; or use details and summary, which open with the keyboard and expose their state with no script; or, as the smallest change, show the submenu on :focus-within as well as :hover. The last two pass 2.1.1 but each leaves a gap, so the button is the one to build.
Test with: keyboard, screen reader, zoom, touch
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/ https://www.w3.org/WAI/tutorials/menus/flyout/ https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details

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.

## What happens

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.

## Why it fails

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.

## Who is affected

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.

## What automation and AI miss

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.

## Before: a submenu shown on hover

```html
<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>
```

```css
.has-sub { position: relative; }
.has-sub > ul { display: none; position: absolute; top: 100%; left: 0; }
.has-sub:hover > ul { display: block; }
```

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.

## After: a button that opens the submenu

```html
<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>
```

```js
for (const item of document.querySelectorAll('.has-sub')) {
  const button = item.querySelector('button');
  const list = document.getElementById(button.getAttribute('aria-controls'));
  const setOpen = (open) => {
    button.setAttribute('aria-expanded', String(open));
    list.hidden = !open;
  };
  button.addEventListener('click', () => setOpen(list.hidden));
  item.addEventListener('keydown', (event) => {
    if (event.key === 'Escape' && !list.hidden) {
      setOpen(false);
      button.focus();
    }
  });
  // Close when focus leaves the item, and leave focus where the user put it.
  item.addEventListener('focusout', (event) => {
    if (!item.contains(event.relatedTarget)) setOpen(false);
  });
}
```

```css
.has-sub { position: relative; }
.has-sub > ul { position: absolute; top: 100%; left: 0; }
```

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".

## After: details and summary

```html
<nav aria-label="Main">
  <ul class="menu">
    <li><a href="/">Home</a></li>
    <li>
      <details name="main-menu" class="has-sub">
        <summary>Products</summary>
        <ul>
          <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>
      </details>
    </li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>
```

```css
.has-sub { position: relative; }
.has-sub > ul { position: absolute; top: 100%; left: 0; }
```

With no script, `summary` opens and closes with Enter and Space, a screen reader announces it as expanded or collapsed, and the links inside are out of the Tab order while it is closed. The same `name` on each `details` in the menu makes opening one close the others. This passes 2.1.1 and 4.1.2, but the browser closes a `details` only when its summary is activated: not on Escape, and not when focus moves on. When the open list sits over the page, as it does here, it stays open after Tab leaves About and can cover the link people tab to next, which fails 2.4.11 Focus Not Obscured. It is enough on its own only where the open list pushes the page down instead of covering it, as in a stacked menu on a narrow screen.

## After: open on focus as well as hover

```css
.has-sub { position: relative; }
.has-sub > ul { display: none; position: absolute; top: 100%; left: 0; }
.has-sub:hover > ul,
.has-sub:focus-within > ul { display: block; }
```

This keeps the markup from the Before and changes one selector. When Products has focus, `:focus-within` shows the list, so the next Tab moves into Lamps, Chairs and Rugs, and the list stays open until focus leaves the item. Every page can now be reached with a keyboard, which passes 2.1.1. It is not enough on its own. The list opens on focus and covers the page, and with CSS alone there is no way to close it with Escape, which 1.4.13 Content on Hover or Focus requires. Every submenu link becomes a Tab stop on the way to About, which adds up across a large menu. Products is still announced as "Products, link", with nothing to say a list sits under it.

## Which option to use

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.

## Implementation decisions

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](/learn/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](/learn/current-page) explains.

## Verify the fix

1. Put the mouse aside and Tab through the navigation from the start of the page. Open each submenu with Enter or Space where it has a button or summary, and reach every link inside it.
2. Check that each opening control says whether it is open: `aria-expanded` or the `open` attribute matches the list on screen after every toggle, and a closed list's links are skipped by Tab.
3. Open a submenu and press Escape: it closes and focus stays on, or returns to, its opening control. Open it again and leave with Tab and with Shift + Tab: the open list never covers the control that has focus.
4. With a screen reader, move through the navigation with Tab and with the arrow keys, and confirm every page can be found and that the opening control is announced as expanded or collapsed.
5. At 200% zoom and on a touch screen, open each submenu and reach each of its links.

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.
