DEV3 Technologies home

Accessible mega-menu nav walker

A Walker_Nav_Menu subclass that renders disclosure buttons, aria-expanded state and Escape-to-close for dropdown menus.

WordPressNavigationmenuwalkerariaa11y

Code

<?php
/**
 * Navigation walker with disclosure buttons.
 *
 * @package Acme_Theme
 */

defined( 'ABSPATH' ) || exit;

class Acme_Mega_Walker extends Walker_Nav_Menu {

	public function start_lvl( &$output, $depth = 0, $args = null ) {
		$output .= '<ul class="acme-panel acme-panel--depth-' . (int) ( $depth + 1 ) . '" hidden>';
	}

	public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) {
		$item       = $data_object;
		$has_kids   = in_array( 'menu-item-has-children', (array) $item->classes, true );
		$is_current = in_array( 'current-menu-item', (array) $item->classes, true );

		$output .= '<li class="acme-item' . ( $has_kids ? ' has-panel' : '' ) . '">';

		$output .= sprintf(
			'<a href="%1$s"%2$s>%3$s</a>',
			esc_url( $item->url ),
			$is_current ? ' aria-current="page"' : '',
			esc_html( $item->title )
		);

		if ( $has_kids ) {
			$output .= sprintf(
				'<button type="button" class="acme-disclose" aria-expanded="false" aria-label="%s"></button>',
				/* translators: %s: menu item title */
				esc_attr( sprintf( __( 'Toggle submenu for %s', 'acme-theme' ), $item->title ) )
			);
		}
	}
}

// Usage:
// wp_nav_menu( array( 'theme_location' => 'primary', 'walker' => new Acme_Mega_Walker(), 'container' => 'nav',
//                     'container_aria_label' => __( 'Primary', 'acme-theme' ) ) );

How it works

What it solves

Hover-only dropdowns lock out keyboard and screen reader users. This walker renders each submenu behind a real button that announces its state, so a mega menu works with touch, mouse and keyboard alike.

How it works

The walker overrides start_lvl to emit a hidden panel list and start_el to print the link plus, for parent items, a sibling button with aria-expanded. A separate button is used rather than hijacking the link, so the parent page stays reachable. The script toggles the panel's hidden attribute, closes it on Escape while returning focus to the button, and closes any open panel when you click elsewhere.

Current items get aria-current="page".

Adapting it

Style .acme-panel as a wide grid for the mega layout and position it under the item. Add the depth class to switch column counts. If you also want hover opening, add it in CSS with :hover only as an enhancement, never as the sole trigger. Set the menu location name in register_nav_menus().

Pitfalls

  • Walker method signatures changed in WordPress 5.9; the $data_object parameter name is the current one.
  • Do not forget end_el and end_lvl defaults; they close the tags, so do not override them carelessly.
  • Hidden panels are removed from the accessibility tree, which is what you want.
  • Test with the keyboard alone before shipping.

How to use it

Save the class in inc/, require it from functions.php, and enqueue assets/mega-menu.js in the footer.

Pass new Acme_Mega_Walker() as the walker argument to wp_nav_menu().

Add CSS so [hidden] panels stay hidden and open ones appear under their parent.

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets