Skip to content
← All solutions

Accessibility / JavaScript

A collapsed menu can still contain keyboard stops

Keep hidden navigation out of the tab order with inert, then restore focus when the menu opens and closes.

Applies to

An HTML navigation panel collapsed with CSS, with JavaScript controlling its open state. Requires browser support for the inert attribute.

The symptom

Pressing Tab moves focus into links in a menu that appears closed. The visible page and keyboard navigation disagree.

Why it happened

Collapsing the panel visually did not make its descendants non-interactive. The site header originally changed its open class and aria-expanded value without making the closed panel inert.

The fix

Start the closed panel with inert. Remove it before focusing the first link on open. Restore it on close, update aria-expanded, and return focus to the toggle when closing with Escape. aria-hidden alone is not a substitute for removing keyboard stops.

<button id="toggle" aria-expanded="false" aria-controls="menu">Menu</button>
<nav id="menu" inert aria-hidden="true">
  <a href="#content">Read the page</a>
</nav>
<main id="content" tabindex="-1">Page content</main>
<script>
  const toggle = document.querySelector('#toggle');
  const menu = document.querySelector('#menu');
  function setOpen(open, restoreFocus = false) {
    menu.inert = !open;
    menu.setAttribute('aria-hidden', String(!open));
    menu.classList.toggle('open', open);
    toggle.setAttribute('aria-expanded', String(open));
    if (open) menu.querySelector('a')?.focus();
    else if (restoreFocus) toggle.focus();
  }
  toggle.addEventListener('click', () => setOpen(menu.inert));
  menu.addEventListener('keydown', event => {
    if (event.key === 'Escape') setOpen(false, true);
  });
</script>

Check the result

  1. Open the standalone reproduction below. In the broken example, focus its Menu button and press Tab: focus enters the visually collapsed panel.
  2. In the fixed example, focus its Menu button and press Tab: focus skips the closed panel and reaches the next visible link.
  3. Open the fixed menu. Its first link receives focus. Press Escape: the menu closes, aria-expanded becomes false, and focus returns to Menu. Tab again skips the closed links.

Where this stops applying

This is a disclosure navigation menu, not a modal dialog or an ARIA menu widget. It does not need a modal focus trap. A full header also needs checks for navigation, viewport changes and outside clicks. The reduced example demonstrates the closed, open and Escape paths only.

Source and evidence

Added inert and aria-hidden to the initially closed panel, then synchronized both in the existing open and close handlers. The example is reduced from that change.

Source file: apps/portfolio/src/components/SiteHeader.astro

Revision: d2039ad6af4fa2c3906f889d13bda7bac564b533

The source repository is private. The public reproduction lets you inspect and test the reduced behavior without repository access. A source revision records a change; it does not prove compatibility with your application.

The public browser example checks closed-panel Tab navigation, opening and Escape focus return. It is not the complete site header.

Browse all fixes