🚶 Walker Classes for Custom Menu Output
Customize WordPress menu HTML output completely
Create advanced navigation structures with Walker_Nav_Menu
Learning Objectives
- Understand Walker class architecture
- Extend Walker_Nav_Menu class
- Override walker methods for custom output
- Add custom HTML to menu items
- Create Bootstrap/Foundation compatible menus
- Build mega menus with Walker classes
- Add icons and badges to menu items
- Implement accessibility enhancements
Understanding Walker Classes
Walker classes in WordPress are used to traverse and display hierarchical data structures like menus, categories, and pages. The Walker_Nav_Menu class specifically handles navigation menu output.
Key Concept
Walker Class Hierarchy
- Walker: Base abstract class
- Walker_Nav_Menu: Extends Walker for navigation menus
- Your_Custom_Walker: Extends Walker_Nav_Menu
Walker_Nav_Menu Methods
| Method | Description | When Called |
|---|---|---|
start_lvl() |
Starts the list before sub-elements | Before outputting child menu items |
end_lvl() |
Ends the list after sub-elements | After outputting child menu items |
start_el() |
Starts the element output | Before each menu item |
end_el() |
Ends the element output | After each menu item |
Walker Method Execution Flow
start_lvl()
→
start_el()
→
[item content]
→
end_el()
→
end_lvl()
Basic Custom Walker
Simple Custom Walker Class
<?php
/**
* Custom Navigation Walker
*/
class MyTheme_Walker_Nav_Menu extends Walker_Nav_Menu {
/**
* Starts the list before the elements are added
*/
public function start_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat( "\t", $depth );
$classes = array( 'sub-menu', 'dropdown-menu' );
if ( $depth > 0 ) {
$classes[] = 'submenu-depth-' . $depth;
}
$class_names = join( ' ', $classes );
$output .= "\n$indent<ul class=\"$class_names\">\n";
}
/**
* Ends the list after the elements are added
*/
public function end_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat( "\t", $depth );
$output .= "$indent</ul>\n";
}
/**
* Start the element output
*/
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
// Add depth class
$classes[] = 'menu-item-depth-' . $depth;
// Add active class
if ( in_array( 'current-menu-item', $classes ) ) {
$classes[] = 'active';
}
// Add dropdown class if has children
if ( in_array( 'menu-item-has-children', $classes ) ) {
$classes[] = 'has-dropdown';
}
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '<li' . $id . $class_names .'>';
$attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
$attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
$attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
$attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
// Add dropdown toggle
if ( in_array( 'menu-item-has-children', $classes ) ) {
$attributes .= ' class="dropdown-toggle" data-toggle="dropdown"';
}
$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
$item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
// Add dropdown arrow
if ( in_array( 'menu-item-has-children', $classes ) ) {
$item_output .= ' <span class="dropdown-arrow">▼</span>';
}
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
/**
* Ends the element output
*/
public function end_el( &$output, $item, $depth = 0, $args = null ) {
$output .= "</li>\n";
}
}
Using the Custom Walker
<?php
// In your theme template
wp_nav_menu( array(
'theme_location' => 'primary',
'menu_class' => 'navbar-nav',
'container' => false,
'walker' => new MyTheme_Walker_Nav_Menu(),
) );
Bootstrap 5 Navigation Walker
Bootstrap 5 Compatible Walker
<?php
/**
* Bootstrap 5 Navigation Walker
*/
class Bootstrap_5_Walker_Nav_Menu extends Walker_Nav_Menu {
// Start Level
function start_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat( "\t", $depth );
$submenu = ( $depth > 0 ) ? ' dropdown-submenu' : '';
$output .= "\n$indent<ul class=\"dropdown-menu$submenu depth_$depth\">\n";
}
// Start Element
function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$li_attributes = '';
$class_names = $value = '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
// Add Bootstrap classes
$classes[] = 'nav-item';
$classes[] = 'menu-item-' . $item->ID;
if ( in_array( 'current-menu-item', $classes ) ) {
$classes[] = 'active';
}
if ( in_array( 'menu-item-has-children', $classes ) ) {
$classes[] = 'dropdown';
if ( $depth > 0 ) {
$classes[] = 'dropdown-submenu';
}
}
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
$class_names = ' class="' . esc_attr( $class_names ) . '"';
$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '<li' . $id . $class_names . $li_attributes . '>';
// Link attributes
$attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
$attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
$attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
$attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
// Add Bootstrap classes to links
$link_classes = 'nav-link';
if ( in_array( 'menu-item-has-children', $classes ) ) {
$link_classes .= ' dropdown-toggle';
$attributes .= ' data-bs-toggle="dropdown" aria-expanded="false"';
}
if ( in_array( 'current-menu-item', $classes ) ) {
$link_classes .= ' active';
$attributes .= ' aria-current="page"';
}
if ( $depth > 0 ) {
$link_classes = 'dropdown-item';
}
$attributes .= ' class="' . $link_classes . '"';
$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
// Add icon support
if ( ! empty( $item->description ) ) {
$item_output .= '<i class="' . esc_attr( $item->description ) . '"></i> ';
}
$item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}
Bootstrap Navbar Implementation
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="<?php echo home_url(); ?>">
<?php bloginfo('name'); ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'navbar-nav ms-auto',
'fallback_cb' => false,
'depth' => 2,
'walker' => new Bootstrap_5_Walker_Nav_Menu()
) );
?>
</div>
</div>
</nav>
Mega Menu Walker
Mega Menu Walker Class
<?php
/**
* Mega Menu Walker
*/
class MegaMenu_Walker extends Walker_Nav_Menu {
private $mega_menu_items = array();
function start_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat( "\t", $depth );
if ( $depth === 0 ) {
$output .= "\n$indent<div class=\"mega-menu-wrapper\">\n";
$output .= "$indent<div class=\"mega-menu-content\">\n";
$output .= "$indent<ul class=\"mega-menu-list\">\n";
} else {
$output .= "\n$indent<ul class=\"sub-menu\">\n";
}
}
function end_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat( "\t", $depth );
if ( $depth === 0 ) {
$output .= "$indent</ul>\n";
// Add featured content area
$output .= $this->get_mega_menu_featured_content();
$output .= "$indent</div>\n";
$output .= "$indent</div>\n";
} else {
$output .= "$indent</ul>\n";
}
}
function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
// Check for mega menu class
if ( in_array( 'mega-menu', $classes ) && $depth === 0 ) {
$classes[] = 'has-mega-menu';
$this->mega_menu_items[] = $item->ID;
}
// Add column classes for mega menu items
if ( $depth === 1 && in_array( $item->menu_item_parent, $this->mega_menu_items ) ) {
$classes[] = 'mega-menu-column';
}
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '<li' . $id . $class_names .'>';
$attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
$attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
$attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
$attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
// Add heading class for column titles
if ( $depth === 1 && in_array( $item->menu_item_parent, $this->mega_menu_items ) ) {
$attributes .= ' class="mega-menu-heading"';
}
$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
// Add custom icon from menu item description
if ( ! empty( $item->description ) && $depth === 0 ) {
$item_output .= '<span class="menu-icon">' . $item->description . '</span>';
}
$item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
private function get_mega_menu_featured_content() {
$output = '<div class="mega-menu-featured">';
$output .= '<h3>Featured Content</h3>';
// Get recent posts or featured content
$recent_posts = wp_get_recent_posts( array(
'numberposts' => 2,
'post_status' => 'publish'
) );
foreach( $recent_posts as $post ) {
$output .= '<div class="featured-item">';
$output .= '<a href="' . get_permalink( $post['ID'] ) . '">';
$output .= get_the_post_thumbnail( $post['ID'], 'thumbnail' );
$output .= '<h4>' . $post['post_title'] . '</h4>';
$output .= '</a>';
$output .= '</div>';
}
$output .= '</div>';
return $output;
}
}
Accessible Navigation Walker
WCAG Compliant Walker
<?php
/**
* Accessible Navigation Walker
*/
class Accessible_Walker_Nav_Menu extends Walker_Nav_Menu {
function start_lvl( &$output, $depth = 0, $args = null ) {
$indent = str_repeat( "\t", $depth );
$output .= "\n$indent<ul class=\"sub-menu\" role=\"menu\" aria-label=\"submenu\">\n";
}
function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
// Add ARIA role
$li_attributes = ' role="none"';
$output .= $indent . '<li' . $id . $class_names . $li_attributes . '>';
$attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
$attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
$attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
$attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
// Add ARIA attributes
$attributes .= ' role="menuitem"';
// Add aria-current for current page
if ( in_array( 'current-menu-item', $classes ) ) {
$attributes .= ' aria-current="page"';
}
// Add aria-haspopup for parent items
if ( in_array( 'menu-item-has-children', $classes ) ) {
$attributes .= ' aria-haspopup="true" aria-expanded="false"';
}
// Add descriptive aria-label if needed
if ( $item->description ) {
$attributes .= ' aria-label="' . esc_attr( $item->title . ': ' . $item->description ) . '"';
}
$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
// Add screen reader text for external links
if ( strpos( $item->url, home_url() ) === false ) {
$item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
$item_output .= '<span class="screen-reader-text"> (opens in a new tab)</span>';
} else {
$item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
}
// Add dropdown indicator for screen readers
if ( in_array( 'menu-item-has-children', $classes ) ) {
$item_output .= '<span aria-hidden="true" class="dropdown-arrow">▼</span>';
$item_output .= '<span class="screen-reader-text"> (has submenu)</span>';
}
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}
Icon Menu Walker
Walker with Icon Support
<?php
/**
* Icon Menu Walker - Uses custom field for icons
*/
class Icon_Walker_Nav_Menu extends Walker_Nav_Menu {
function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
// Check for icon class in classes array
$icon_class = '';
foreach ( $classes as $class ) {
if ( strpos( $class, 'fa-' ) === 0 || strpos( $class, 'icon-' ) === 0 ) {
$icon_class = $class;
break;
}
}
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '<li' . $id . $class_names .'>';
$attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
$attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
$attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
$attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
// Add icon if exists
if ( $icon_class ) {
$item_output .= '<i class="menu-icon ' . esc_attr( $icon_class ) . '"></i> ';
}
// Add badge for new items (using description field)
$title = apply_filters( 'the_title', $item->title, $item->ID );
if ( $item->description == 'new' ) {
$title .= ' <span class="badge badge-new">NEW</span>';
} elseif ( $item->description == 'hot' ) {
$title .= ' <span class="badge badge-hot">HOT</span>';
}
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}
Walker Use Cases
🎨 Framework Integration
Create walkers for Bootstrap, Foundation, Tailwind CSS
📱 Mobile Menus
Custom mobile navigation with accordions
🏢 Mega Menus
Complex multi-column dropdown menus
♿ Accessibility
Enhanced ARIA attributes and keyboard navigation
🎯 Custom Markup
Completely custom HTML structure
🏷️ Badges & Icons
Add icons, badges, and custom elements
Best Practices
Walker Class Best Practices
- Extend existing walkers: Don't reinvent the wheel
- Maintain backward compatibility: Support standard menu features
- Escape output properly: Use esc_attr(), esc_html(), etc.
- Preserve menu item classes: Don't remove WordPress classes
- Add accessibility attributes: Include ARIA labels and roles
- Document your walker: Explain custom features and usage
- Test thoroughly: Check all menu configurations
- Keep it performant: Avoid heavy operations in walker methods
Always sanitize and escape data in walker classes. Menu items can contain user-generated content that must be properly secured.
Practice Exercise
Create Custom Navigation Walker