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 callapply_filters('nav_menu_link_attributes', ...)insidestart_el(). Plugins rely on this hook to add link attributes. - To make dropdown menus work with keyboards and screen readers, set
aria-haspopupandaria-expandedcorrectly. - If you set
fallback_cbinwp_nav_menu()tofalse, nothing is output when no menu is assigned. By default it falls back towp_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)