WordPress allows using so-called Walker classes for traversing and displaying elements in an hierarchical structure. In this post we’ll learn about how to create, implement and customize our own walker class to customize our menu output.

The most known use of customization with Walker classes in WordPress is for menus, but in reality WordPress uses Walker classes for a whole bunch of cases, for example outputting taxonomy hierarchies, comment hierarchies, wp_list_pages() and wp_list_categories(). They all extend a general Walker class. We will extend the Walker_Nav_Menu which is used for menus in WordPress.

Because we extend another class we need only add the functions we wish to override. If a function does not exist in our class, WordPress will run the parent class’ (the class we extend) function instead.

Preparation

You can add your walker class in your plugin files, theme’s function.php or any PHP file included by functions.php (for cleaner code). You start by defining your class by a name of your choosing (make sure the class name is unique, and this includes possible class names in WordPress core!) extending Walker_Nav_Menu:

class AWP_Menu_Walker extends Walker_Nav_Menu {
	
}

In order to tell WordPress to use our walker, we define this in our wp_nav_menu() calls. This function is responsible for outputting a menu and you probably has at least one in your theme for the main menu.

In the argument array to wp_nav_menu() you add a new element with the key ‘walker’ and creates a new instance of your walker class like so:

wp_nav_menu([
    'theme_location' => 'primary',
    'menu_class' => 'main-menu',
    'container' => 'nav',
    'container_class' => 'header__main-nav',
    'walker' => new AWP_Menu_Walker()
]);

If you refresh your site you should see no change. This is because our class does not override any of the parent’s functions, and thus WordPress simply runs the normal menu walker functions when outputting the menu, just like before we told it to use our walker.

Overview of functions we can override in Walker_Nav_Menu

The following are functions you can add to your custom walker class to override the parenting class Walker_Nav_Menu funtions:

The first four are functions that are simply responsible for outputting, and they all require you to append to a string – the first parameter variable. It’s important to know that you don’t echo anything out here, everything is supposed to be built up as a string.

start_lvl

The function start_lvl is responsible for outputting the HTML for the start of a new level. In short it should output the starting <ul>.

function start_lvl(&$output, $depth=0, $args=null) { }

The first parameter, $output – passed by reference, is the string you’ll append your output to. $depth is an integer signaling which level you’re at; 0 for top-level, 1 for direct child of top-level, and so on. $args is an object of all arguments provided in wp_nav_menu().

end_lvl

The end_lvl function is responsible for outputting the HTML for the end of a level. This is usually just the closing </ul>.

function end_lvl(&$output, $depth=0, $args=null) { }

The parameters are the exact same as for start_lvl above.

start_el

This function is responsible for outputting each element’s HTML. In short it should output the starting <li> and the <a> tag with the link title inside.

function start_el(&$output, $item, $depth=0, $args=null, $id=0) { }

The first argument, $output, is as usual the string you’ll append the output to. The second argument, $item, is the menu item object – and this is where you’ll fetch most of the data for outputting the menu item. If the menu link is a post menu item, you’d get the post object here. Regardless of menu type, you’ll also get some additional useful elements; such as classes, url, title, and description.

The third argument, $depth, is an integer telling you which level we’re at. Level 0 is top-level, 1 is direct child of top-level, and so on. The fourth argument, $args, is an object of all arguments provided to wp_nav_menu(). The fifth parameter, $id, is the current menu item ID.

end_el

The end_el function is responsible for outputting the closing of an element. Usually it would just output the </li> tag.

function end_el(&$output, $item, $depth=0, $args=null) { }

The arguments for end_el are the same as start_el above except that the function doesn’t have the fifth parameter, $id.

display_element

The function display_element is an inherited function from the general Walker class, and is the function responsible for traversing. This is the function that calls all of the above functions in turn.

I’m including this here because in some cases, for example if you want to prevent traversing a whole branch, you’d use this function for that.

function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output) { }

The first argument, $element, is the menu item object – this is what is passed down as $item in the above functions. The second argument, $children_elements – passed by reference, contains all child elements this function will traverse. $max_depth, the third argument, is an integer that signals how deep we should traverse, and the fourth argument, $depth, is the depth we’re currently at. The fifth argument, $args, is the arguments passed to the function that called the walker (for menus it would be the arguments provided to wp_nav_menu()), and the final argument, $output – passed by reference, is the output which is passed down as first argument in all of the above functions.

Modifying the output of each element

In the overview above you should see that the function start_el() is the one responsible for outputting the HTML for a single menu element. Let’s start by overriding this function in our walker class with a simple example.

Example: preventing adding links for ‘#’ elements

Let’s make sure that any ‘#‘ links gets a <span> element instead of a link tag, to avoid refreshing the page.

class AWP_Menu_Walker extends Walker_Nav_Menu {
	function start_el(&$output, $item, $depth=0, $args=[], $id=0) {
		$output .= "<li class='" .  implode(" ", $item->classes) . "'>";

		if ($item->url && $item->url != '#') {
			$output .= '<a href="' . $item->url . '">';
		} else {
			$output .= '<span>';
		}

		$output .= $item->title;

		if ($item->url && $item->url != '#') {
			$output .= '</a>';
		} else {
			$output .= '</span>';
		}
	}
}

We’ll start the element by appending a <li> tag to $output. We want to make sure that WordPress’ default classes (for example ‘menu-item’, ‘menu-item-has-children’ etc), as well as classes entered manually in Menu editor gets added to our list element. We glue the classes provided as an array in $item->classes using the PHP function implode() separating each element with a space.

At line #5-9 and #13-17 we handle the conditional output of the wrapping element. We output a <a> tag, unless the element’s URL is ‘#‘ in which case we provide a <span> tag instead. At line #11 we simply output the link’s text, which resides in $item->title.

This is all we need for making sure all menu elements that has ‘#‘ as URL isn’t clickable!

If you are doing this in a styled theme, keep in mind that you might lose some styling if the theme has styled the <a> tag directly. You can solve this by changing the styling and possibly adding a class to the span element.

Example: displaying menu item descriptions

As an example another thing you can do here is outputting the menu description. This exists, but is not activated as default. In WordPress Menu editor you need to click “Screen Options” in the top right, and check off for showing “Description”:

This allows the user to enter a description to each element. You can output this description in your walker class. Let’s say you only want to show description for the top-level items, as this is a part of your theme’s design. You can simply check if the $item has a description and if $depth is 0, like so:

		...
		$output .= $item->title;

		if ($depth == 0 && !empty($item->description)) {
			$output .= '<span class="description">' . $item->description . '</span>';
		}
		...

Example: Adding dropdown carets

A more common and useful example is adding a “caret”, an icon that signals that this menu item has a dropdown menu (has child elements).

Example of carets in action – behind “Blog” and “News”

You’ll need to figure out your caret HTML output. In my case I’m outputting an <i> item with specific classes for a nice down arrow available by the Fontawesome library which provides thousands of icons. You also want to ensure this caret only outputs on elements that has children. The best way I’ve found to figure out if the current element has children, is by referencing the walker object (yes, which is our walker itself, but also the classes it extends!) in $args, and checking the boolean has_children. Outputting a caret is as simple as:

if ($args->walker->has_children) {
	$output .= '<i class="caret fa fa-angle-down"></i>';
}

The complete walker class would look like this:

class AWP_Menu_Walker extends Walker_Nav_Menu {
	function start_el(&$output, $item, $depth=0, $args=[], $id=0) {
		$output .= "<li class='" .  implode(" ", $item->classes) . "'>";

		if ($item->url && $item->url != '#') {
			$output .= '<a href="' . $item->url . '">';
		} else {
			$output .= '<span>';
		}

		$output .= $item->title;

		if ($item->url && $item->url != '#') {
			$output .= '</a>';
		} else {
			$output .= '</span>';
		}

		if ($args->walker->has_children) {
			$output .= '<i class="caret fa fa-angle-down"></i>';
		}
	}
}

And that’s all you need for ensuring your menu gets nice caret icons on parent elements and that ‘#‘ links won’t be clickable.

If you want the caret icon to change, for example into an up arrow when the dropdown is active, you will need to add this with Javascript to your theme.

As the examples above suggest, you can manipulate the output however you’d like, based on any conditionals. You can for example modify the output based on if a certain class is present (for example a class manually entered in Menu editor) by looking for the class in $item->classes, or you can manipulate (for example capitalize) the outputted item text provided in $item->title.

Providing arguments to your walker through your wp_nav_menu

I would like to mention another useful thing. Remember that $args contains all arguments provided to wp_nav_menu(). This includes for example theme_location and others, so if you can modify the output only for specific theme locations – for example main menu. But you can actually provide any custom arguments!

Say you are outputting the same menu multiple times, for example one for desktop and again for mobile. Or you want your walker to manipulate the items only when they are output by wp_nav_menu() in your theme, and not when the menu is added through a widget? Perhaps you want your walker to handle the output differently in these cases?

You can provide any custom arguments to wp_nav_menu(). As a simple example, I’ll add a boolean ‘show_carets‘ to the arguments to ensure that carets are added only in those cases I want them – instead of my walker class adding carets to all menus.

wp_nav_menu([
    'theme_location' => 'primary',
    'menu_class' => 'main-menu',
    'container' => 'nav',
    'container_class' => 'header__main-nav',
    'walker' => new AWP_Menu_Walker(),
    'show_carets' => true
]);

Then I can simply change my caret-adding piece of code above (line #19-21) into checking whether or not show_carets is present and true in $args, like so:

if ($args->show_carets && $args->walker->has_children) {
	$output .= '<i class="caret fa fa-angle-down"></i>';
}

You can add any arguments you’d like ensuring your walker only customizes the menus you want to. For example simple booleans for different cases, e.g. is_mobile_menu, or anything else you need.

And that’s about it. Feel free to experiment, and let me know if you have any questions or suggestions below!