2026年11月12日

2026年11月12日

Customize WordPress Navigation Menus with Walker_Nav_Menu

This article is a localized edition of the Japanese original. Read the Japanese original

Introduction

"I want full control over the HTML of my header navigation." "I want to build a dropdown menu or a hamburger menu." "I want to add icons or badges to menu items." By customizing the Walker_Nav_Menu class, you can take complete control of WordPress's navigation output.

Symptoms and Causes

With the default output of wp_nav_menu(), it is hard to control CSS classes or insert your own HTML, so you sometimes cannot build the structure a modern design needs. Extending Walker_Nav_Menu lets you fully customize the output HTML.

How to Do It

Step 1: Register navigation menus

// functions.php: register navigation menu locations
add_action( 'after_setup_theme', function() {
    register_nav_menus( [
        'primary'   => 'Global Navigation',
        'footer'    => 'Footer Navigation',
        'mobile'    => 'Mobile Menu',
        'social'    => 'Social Links',
    ] );
} );

Step 2: Output a basic navigation with wp_nav_menu()

// header.php: global navigation
wp_nav_menu( [
    'theme_location'  => 'primary',      // ID of the registered menu location
    'container'       => 'nav',          // Wrapper element (false to disable)
    'container_class' => 'global-nav',
    'container_id'    => 'global-nav',
    'menu_class'      => 'nav-list',
    'menu_id'         => 'nav-list',
    'echo'            => true,
    'fallback_cb'     => false,          // Output nothing when no menu is assigned
    'depth'           => 3,              // How many menu levels to display
    'items_wrap'      => '<ul id="%1$s" class="%2$s" role="list">%3$s</ul>',
] );

Step 3: Implement custom output with Walker_Nav_Menu

// functions.php: custom NavWalker
class My_Nav_Walker extends Walker_Nav_Menu {

    // Opening tag of a menu item
    public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
        $classes   = empty( $item->classes ) ? [] : (array) $item->classes;
        $classes[] = 'nav-item';
        $classes[] = 'menu-item-' . $item->ID;

        // Menu item for the current page
        if ( in_array( 'current-menu-item', $classes, true ) ) {
            $classes[] = 'is-active';
        }

        // When the item has a submenu
        $has_children = in_array( 'menu-item-has-children', $classes, true );
        if ( $has_children ) {
            $classes[] = 'has-dropdown';
        }

        $class_str = implode( ' ', array_filter( array_unique( $classes ) ) );

        $output .= '<li class="' . esc_attr( $class_str ) . '">';

        // Anchor tag attributes
        $atts           = [];
        $atts['href']   = ! empty( $item->url ) ? $item->url : '#';
        $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
        $atts['target'] = ! empty( $item->target ) ? $item->target : '';
        $atts['rel']    = ! empty( $item->xfn ) ? $item->xfn : '';

        // aria-current for the current page
        if ( in_array( 'current-menu-item', $classes, true ) ) {
            $atts['aria-current'] = 'page';
        }

        // aria-haspopup when the item has a submenu
        if ( $has_children ) {
            $atts['aria-haspopup'] = 'true';
            $atts['aria-expanded'] = 'false';
        }

        $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

        $attrs_str = '';
        foreach ( $atts as $attr => $value ) {
            if ( is_scalar( $value ) && '' !== $value ) {
                $attrs_str .= ' ' . $attr . '="' . esc_attr( $value ) . '"';
            }
        }

        $title = apply_filters( 'the_title', $item->title, $item->ID );

        $output .= '<a' . $attrs_str . '>';
        $output .= '<span class="nav-label">' . esc_html( $title ) . '</span>';

        // Toggle button when the item has a submenu
        if ( $has_children && 0 === $depth ) {
            $output .= '<button class="dropdown-toggle" aria-label="Open submenu">'
                     . '<svg viewBox="0 0 12 8" width="12" aria-hidden="true"><path d="M1 1l5 5 5-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>'
                     . '</button>';
        }

        $output .= '</a>';
    }

    // Opening tag of a submenu
    public function start_lvl( &$output, $depth = 0, $args = null ) {
        $output .= '<ul class="dropdown-menu" role="list">';
    }

    // Closing tag of a submenu
    public function end_lvl( &$output, $depth = 0, $args = null ) {
        $output .= '</ul>';
    }
}

// Output the menu using the Walker
wp_nav_menu( [
    'theme_location' => 'primary',
    'container'      => 'nav',
    'container_class' => 'global-nav',
    'walker'         => new My_Nav_Walker(),
] );

Step 4: Build the dropdown menu with CSS

/* Global navigation */
.global-nav {
    position: relative;
}

.nav-list {
    display: flex;
    align-items: center;
    gap: 0;
    list-style: none;
    margin: 0;
    padding: 0;
}

.nav-item {
    position: relative;
}

.nav-item > a {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 1rem 1.25rem;
    color: #1d2327;
    text-decoration: none;
    font-weight: 500;
    font-size: 0.9rem;
    transition: color 0.2s;
    white-space: nowrap;
}

.nav-item > a:hover,
.nav-item.is-active > a {
    color: #2271b1;
}

/* Dropdown menu */
.dropdown-menu {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 220px;
    background: #fff;
    border: 1px solid #dcdcde;
    border-radius: 4px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.12);
    list-style: none;
    margin: 0;
    padding: 0.5rem 0;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
    z-index: 100;
}

.nav-item.has-dropdown:hover > .dropdown-menu,
.nav-item.has-dropdown:focus-within > .dropdown-menu,
.nav-item.has-dropdown[data-open="true"] > .dropdown-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.dropdown-menu li a {
    display: block;
    padding: 0.6rem 1.25rem;
    color: #1d2327;
    text-decoration: none;
    font-size: 0.875rem;
    transition: background 0.15s;
}

.dropdown-menu li a:hover {
    background: #f0f6fc;
    color: #2271b1;
}

/* Hamburger button */
.hamburger-btn {
    display: none;
    flex-direction: column;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 8px;
}

.hamburger-btn span {
    display: block;
    width: 24px;
    height: 2px;
    background: #1d2327;
    transition: transform 0.3s, opacity 0.3s;
}

@media (max-width: 768px) {
    .hamburger-btn { display: flex; }

    .nav-list {
        display: none;
        flex-direction: column;
        position: fixed;
        top: 0;
        right: 0;
        width: 280px;
        height: 100vh;
        background: #fff;
        padding: 4rem 1.5rem 2rem;
        box-shadow: -4px 0 20px rgba(0,0,0,0.15);
        overflow-y: auto;
        z-index: 1000;
    }

    .nav-list.is-open { display: flex; }
}

Step 5: Control the hamburger menu and dropdowns with JavaScript

// js/navigation.js
(function() {
    const btn    = document.querySelector('.hamburger-btn');
    const menu   = document.getElementById('nav-list');
    if (!btn || !menu) return;

    // Open and close the hamburger menu
    btn.addEventListener('click', function() {
        const isOpen = menu.classList.toggle('is-open');
        btn.setAttribute('aria-expanded', isOpen);
        btn.setAttribute('aria-label', isOpen ? 'Close menu' : 'Open menu');
        document.body.style.overflow = isOpen ? 'hidden' : '';
    });

    // Close with the Esc key
    document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape' && menu.classList.contains('is-open')) {
            menu.classList.remove('is-open');
            btn.setAttribute('aria-expanded', 'false');
            document.body.style.overflow = '';
            btn.focus();
        }
    });

    // Keyboard operation for dropdowns
    document.querySelectorAll('.dropdown-toggle').forEach(function(toggle) {
        toggle.addEventListener('click', function(e) {
            e.preventDefault();
            const parent = toggle.closest('.has-dropdown');
            const isOpen = parent.getAttribute('data-open') === 'true';
            parent.setAttribute('data-open', !isOpen);
            toggle.setAttribute('aria-expanded', !isOpen);
        });
    });

    // Close when clicking outside the menu
    document.addEventListener('click', function(e) {
        if (!menu.contains(e.target) && !btn.contains(e.target)) {
            menu.classList.remove('is-open');
            btn.setAttribute('aria-expanded', 'false');
            document.body.style.overflow = '';
        }
    });
})();

Caveats

  • When you extend Walker_Nav_Menu, always call apply_filters('nav_menu_link_attributes', ...) inside start_el(). Plugins rely on this hook to add link attributes.
  • To make dropdown menus work with keyboards and screen readers, set aria-haspopup and aria-expanded correctly.
  • If you set fallback_cb in wp_nav_menu() to false, nothing is output when no menu is assigned. By default it falls back to wp_page_menu().

Summary

Customize navigation menus in this order: register locations with register_nav_menus() → output the basics with wp_nav_menu() → extend Walker_Nav_Menu to fully control the HTML structure → implement dropdowns and animations in CSS → handle the hamburger toggle and accessibility in JS. Related articles: How to customize WordPress widget areas, How to add breadcrumbs to WordPress.

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲