reala11y GUIDES Accessiblenavigation menusin WordPress… Jul 24, 2023

July 24, 2023 · reala11y team

Accessible navigation menus in WordPress: keyboard and dropdowns

Build accessible WordPress nav: semantic <nav>, full keyboard operation, accessible submenus, aria-current, and skip-to-nav. Fix the common block and classic-menu issues.

Navigation is the part of a site every visitor touches and the part WordPress themes most reliably get half-right. The markup usually looks fine; the keyboard behaviour usually isn’t. A mouse user hovers a parent item and the submenu appears. A keyboard user tabs to that same item and the submenu either never opens, or opens and traps them, or opens and can’t be dismissed. None of that shows up in a screenshot.

This guide walks the menu patterns that matter for accessibility, with a WordPress angle throughout. It is about the structure and the interaction — not a promise that fixing them makes a site conform on its own.

Start with semantic structure

A navigation region is a <nav> element wrapping a list of links. WordPress’s wp_nav_menu() already emits a <ul> of <li> items; your job is to make sure it lives inside a labelled landmark.

<nav aria-label="Primary">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/pricing">Pricing</a></li>
    <li><a href="/guides">Guides</a></li>
  </ul>
</nav>

The aria-label matters the moment you have more than one <nav> on a page — a primary menu plus a footer menu, say. Without distinct labels, a screen-reader user’s landmark list reads “navigation, navigation, navigation” and they can’t tell which is which. Keep labels short; don’t write aria-label="Primary navigation", because the role already announces “navigation” and you’d get a stutter.

Block themes assemble menus from the Navigation block rather than wp_nav_menu(). The same rule applies: confirm the rendered output is a real <nav> with a list inside, not a stack of <div>s. Inspect the front-end HTML, not the editor.

Mark the current page

WCAG doesn’t have a dedicated “you are here” criterion, but aria-current="page" is one of the cheapest wins in a menu. It tells assistive technology which link corresponds to the current location, and it gives you a styling hook that doesn’t rely on colour alone.

<li><a href="/pricing" aria-current="page">Pricing</a></li>

WordPress adds a current-menu-item class to the active <li> automatically, but that class is invisible to a screen reader. Adding aria-current is a small theme tweak that pays off for everyone navigating by sound.

Make submenus actually operable

This is where most menus fail. A submenu that opens on hover must also open on keyboard focus, stay open while focus is inside it, and close when focus leaves or the user presses Escape.

There are two defensible patterns:

For a typical WordPress site, the disclosure button is the right call:

<li>
  <button aria-expanded="false" aria-controls="sub-products">Products</button>
  <ul id="sub-products" hidden>
    <li><a href="/scanner">Scanner</a></li>
    <li><a href="/fixes">Fixes</a></li>
  </ul>
</li>
const btn = document.querySelector('button[aria-controls="sub-products"]');
const sub = document.getElementById('sub-products');

btn.addEventListener('click', () => {
  const open = btn.getAttribute('aria-expanded') === 'true';
  btn.setAttribute('aria-expanded', String(!open));
  sub.hidden = open;
});

// Close on Escape, return focus to the trigger.
sub.addEventListener('keyup', (e) => {
  if (e.key === 'Escape') {
    btn.setAttribute('aria-expanded', 'false');
    sub.hidden = true;
    btn.focus();
  }
});

The common WordPress trap is a theme or menu plugin that reveals submenus purely with CSS :hover. CSS-only flyouts are unreachable by keyboard and invisible to screen readers — the submenu links exist in the DOM but a keyboard user can never see or trigger them. If your menu opens only on hover, that’s the first thing to fix.

Don’t forget the mobile toggle

The hamburger button is a control, so it needs a name and a state. An unlabelled <button> with just an icon announces as “button” and nothing else.

<button aria-expanded="false" aria-controls="primary-menu">
  <span class="screen-reader-text">Menu</span>
  <svg aria-hidden="true">…</svg>
</button>

Mark the icon aria-hidden="true", give the button a visually-hidden text name, and keep aria-expanded in sync as it opens and closes.

Most sites ship a “skip to content” link (see our skip links and landmarks guide). A complementary “skip to navigation” link helps when the menu sits below other repeated content, letting keyboard users jump straight to it. It’s the same pattern — a focusable link, visible on focus, pointing at the nav’s id.

Where automation helps, and where it doesn’t

A scanner can confirm the structural signals: that a <nav> exists, that duplicated navs carry distinct labels, that the toggle button has an accessible name, that links aren’t empty. That’s real value, found fast. reala11y flags these at the code level rather than papering over them with a visitor-side script.

What a scanner cannot judge is the interaction: whether your submenu opens on focus, whether Escape dismisses it, whether focus order makes sense once you tab in. That’s the honest split — automated tooling, ours included, typically detects roughly 30–40% of WCAG issues by criteria, and keyboard behaviour mostly lives in the other 60%.

The honest takeaway

A good WordPress menu is semantic markup plus disciplined keyboard handling: a real <nav>, aria-current on the active link, submenus that open on focus and close on Escape, and a labelled mobile toggle. Fix the structure, then unplug your mouse and tab through your own header. If you reach every link, open every submenu, and escape every one of them with the keyboard alone, you’ve done the part no scanner can verify for you. For the criteria behind all of this, see the WCAG 2.2 guide.