There are many things about the WordPress nav menu system that I like, and a handful of things I really dislike. The things I dislike happen to trigger my pet peeves: Markup-bloat and non-SMACCS-ish class names. To me, it’s worth a deep dive to correct this situation. However, I don’t want to lose integration with the admin UI, and I don’t want to lose the dynamic class names that core gives us, such as current_page_item
or current-menu-ancestor
. Therefore, I’m not going to replace anything: I’m going to extend the PHP class that draws the nav menus: The Walker_Nav_Menu
class.
I’ll do this by building a plugin that outputs nav menus with whatever markup and classes I want. Along the way, I’ll stop and smell the roses var_dump()
the PHP variables that WordPress exposes to us. This plugin will have the following components:
- A main plugin file to register the plugin and call other files
- A shortcode for outputting the menu
- Some CSS, JS, & SVG to do things like show/hide submenus
- A custom walker class, which will extend the core
Walker_Nav_Menu
class
Of those components, all but the last one will act mainly as placeholders. They’ll offer the minimum amount of code in order to achieve the minimum viable product, and I won’t be exploring them in detail. They will provide just enough of a foundation for me to build a custom walker class.
Assumptions
- Let’s do this on the twentyfifteen theme
- If any other plugins are active, be sure they don’t cause JS or PHP errors. If in doubt, deactivate them
- I’m on WordPress 4.3.1. at the time of writing
The Plugin
I’m going to cite blocks from the finished plugin as we go. You can grab it from my GitHub repo if you’d like to refer to the finished product, or even install it on a WordPress test site.
The Shortcode
The plugin works by registering a shortcode, [csst_nav]
. The shortcode takes one argument, which_menu
, where you can choose which nav menu to output by providing the slug, ID, or title of a nav menu. Here are some examples, where I happen to have a menu called “Legal Links”, with a slug of legal-links
and an ID of 5
:
[csst_nav]
[csst_nav which_menu='legal-links']
[csst_nav which_menu='Legal Links']
[csst_nav which_menu='5']
The shortcode is a wrapper for the wp_nav_menu()
function, which takes a ton of arguments.
Here’s where I depart from the defaults and do what I prefer instead:
menu
: I want to be able to specify which menu to grab.container
: I want less markup, so no container element is needed.menu_class
: I love class names. I’ll give this some classes namespaced for my plugin and for the menu I’m grabbing.echo
: No thank you. I’ll return the menu rather than echo it.items_wrap
: I’ll wrap the items in a<nav>
rather than the default unordered list.before
: I’ll open each menu item as a<span>
, and also get rid of core’s hard-coded<li>
.after
: I’ll close each menu item with a closing</span>
, and also get rid of core’s hard-coded</li>
.before_submenu
: I’ll open each submenu as a<span>
instead of a<ul>
.after_submenu
: I’ll close each submenu with a closing<span>
, rather than a closing</ul>
.walker
: This is why you’re reading this article. I’ll tell WordPress to use our custom walker class.
Some of those arguments, such as before_submenu
and after_submenu
, don’t actually ship with wp_nav_menu()
. That’s okay though, as they still get passed through to the walker class where I can use them however I like.
Here’s what that all looks like in code:
<?php
/**
* The main template tag for this class. Get a custom menu via our walker.
*
* @return string A WordPress custom menu, passed through our walker class.
*/
public function get() {
// The CSS class for our shortcode.
$class = strtolower( __CLASS__ );
// Get a menu from the db.
$which_menu = $this -> which_menu;
/**
* Args for a call to wp_nav_menu().
*
* Some of these args don't get used by wp_nav_menu() per se,
* but we're able to pass them through to our walker class, which does use them.
*/
$menu_args = array(
// Instead of wrapping each menu item as list item, let's do a span.
'after' => '',
// The closing markup after a submenu.
'after_submenu' => '',
// Instead of wrapping each menu item as list item, let's do a span.
'before' => '',
// The opening markup before a submenu.
'before_submenu' => '',
// Nope, we don't need extra markup wrapping our menu.
'container' => FALSE,
// Nope, let's return instead of echo.
'echo' => FALSE,
// Let's use a <nav> instead of a nested list.
'items_wrap' => '<nav role="navigation" class="%2$s">%3$s</nav>',
// Which menu to grab? Takes ID, name, or slug.
'menu' => $which_menu,
// CSS classes for our menu.
'menu_class' => "$class $class-$which_menu",
// Our custom walker.
'walker' => new CSST_Nav_Walker(),
);
// The main content of the shortcode is in fact a call to wp_nav_menu().
$out = wp_nav_menu( $menu_args );
return $out;
}
?>
Alright, enough with the preamble. It’s time to dive into the custom walker class. I love excruciating detail!
The Custom Walker Class
There is something of a hierarchy going on here:
- Core defines an extremely generic class:
Walker
. Its purpose is to iterate through complex structures like multi-dimensional arrays, and do stuff on each member of that structure. - Core then defines a more specific extension of
Walker
, made specifically for digging through navigation menus:Walker_Nav_Menu
. - Finally, I define my own extension of
Walker_Nav_Menu
, calling itCSST_Nav_Walker
.
My custom walker class will extend the following methods from core’s Walker_Nav_Menu
:
start_el()
, which appends the opening markup for menu items, and menu items themselves.end_el()
, which appends the closing markup menu items.start_lvl()
, which appends the opening markup for submenus.end_lvl()
, which appends the closing markup for submenus.
Those are some super generic names, eh? That’s kind of the point: We’re inheriting from Walker
, which is meant to be able to iterate through any kind of structure, for any reason. In that context, specificity is the enemy. Let’s cut through the abstract nomenclature and figure out what each method does for us!
start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 )
This method draws the opening html for a menu item, and the menu item itself. It carries five parameters:
&$output
, which is the all of the HTML for the menu, up until “this” menu item. When I say “this” menu item, understand that this method is called once for each menu item.$item
, which is the WP Post object for this menu item (menu items are in fact posts of thenav_menu_item
post type), plus some additional data particular to nav menus.$depth
, which keeps track of how many levels deep we are in the menu — as in nested submenus.$args
, which is mostly an array of arguments forwp_nav_menu()
. It includes the arguments that we passed in our shortcode callback, plus all of the default values we omitted.$id
, which is documented in the core source as the ID of the current menu item, though I’m not sure if it’s still supported.
Most of these parameters are a bit underwhelming, but some of them carry a ton of useful information. Allow me to var_dump()
!
&$output
Notice that this variable is prefixed with an ampersand, meaning it’s passed by reference. That means the method does not have to return anything, because anything that happens to this variable within the method will affect the variable outside of the method as well. This is also why the var_dump()
gets very large very quickly:
var_dump( esc_html( $output ) );
gets us:
<?php
string(0) ""
string(274) "
Front Page
"
string(1066) "
Front Page
[...] (truncated)
“
This ends up being about 35kb of var_dump()
text, so I’ve greatly truncated it. I’m only showing parts of the first three menu items. That’s the markup for the preceding menu items, at each menu item, which is why we append the current menu item to it.
$item
This parameter gives us the WP Post object for the current menu item, making it by far the most interesting arg in this method.
wp_die( var_dump( $item ) )
gives us:
<?php
object(WP_Post)#358 (40) {
["ID"] => int(68)
["post_author"] => string(1) "1"
["post_date"] => string(19) "2015-10-07 01:05:49"
["post_date_gmt"] => string(19) "2015-10-07 01:05:49"
["post_content"] => string(1) " "
["post_title"] => string(0) ""
["post_excerpt"] => string(0) ""
["post_status"] => string(7) "publish"
["comment_status"] => string(6) "closed"
["ping_status"] => string(6) "closed"
["post_password"] => string(0) ""
["post_name"] => string(2) "68"
["to_ping"] => string(0) ""
["pinged"] => string(0) ""
["post_modified"] => string(19) "2015-10-07 01:05:49"
["post_modified_gmt"] => string(19) "2015-10-07 01:05:49"
["post_content_filtered"] => string(0) ""
["post_parent"] => int(0)
["guid"] => string(33) "http://localhost/wp/csstnav/?p=68"
["menu_order"] => int(1)
["post_type"] => string(13) "nav_menu_item"
["post_mime_type"] => string(0) ""
["comment_count"] => string(1) "0"
["filter"] => string(3) "raw"
["db_id"] => int(68)
["menu_item_parent"] => string(1) "0"
["object_id"] => string(2) "50"
["object"] => string(4) "page"
["type"] => string(9) "post_type"
["type_label"] => string(4) "Page"
["url"] => string(28) "http://localhost/wp/csstnav/"
["title"] => string(10) "Front Page"
["target"] => string(0) ""
["attr_title"] => string(0) ""
["description"] => string(0) ""
["classes"] => array(8) {
[0]=> string(0) ""
[1]=> string(9) "menu-item"
[2]=> string(24) "menu-item-type-post_type"
[3]=> string(21) "menu-item-object-page"
[4]=> string(17) "current-menu-item"
[5]=> string(9) "page_item"
[6]=> string(12) "page-item-50"
[7]=> string(17) "current_page_item"
}
["xfn"] => string(0) ""
["current"] => bool(true)
["current_item_ancestor"] => bool(false)
["current_item_parent"] => bool(false)
}
Pretty neat, right? We could reach into that post object and get a ton of cool stuff like the excerpt, the date, taxonomies. Heck, maybe we could engineer a way to do featured images for nav menu items! In addition to these values that we normally see for posts, there are a couple of new items, such as classes
. That’s where the awesome array of dynamic CSS classes can be found: Things like current-menu-item
. Also of note is object
, which gives us details about what this menu item is linking to: Perhaps a page or a term archive.
$depth
Depth keeps a running tally of many submenus “deep” we are. I don’t have any use for this, but I’m willing to stop and admire what core does with it: They use it to prepend tab characters (as in, literally, “\t”) so that the source code is more readable. At least I’m assuming that’s why. Well played core, well played.
Rather than var_dump()
$depth
, it’s more instructive to just append it to &$output
for each item. You can see how it’s tracking the level for each item:
$args
$args
should look familiar: It’s mostly the array of values that I passed to wp_nav_menu()
in our shortcode. Plus, the default values for any args that I omitted.
var_dump( esc_html( $args ) );
gets us:
<?php
object(stdClass)#341 (16) {
["menu"] => string(13) "a-nested-menu"
["container"] => bool(false)
["container_class"] => string(0) ""
["container_id"] => string(0) ""
["menu_class"] => string(31) "csst_nav csst_nav-a-nested-menu"
["menu_id"] => string(0) ""
["echo"] => bool(false)
["fallback_cb"] => string(12) "wp_page_menu"
["before"] => string(0) ""
["after"] => string(0) ""
["link_before"] => string(0) ""
["link_after"] => string(0) ""
["items_wrap"] => string(46) "%3$s"
["depth"] => int(0)
["walker"] => object( CSST_Nav_Walker )#339 (5) {
["icon"] => string(96) "
<svg class='csst_nav_svg-icon'>
<use xmlns:xlink='http://www.w3.org/1999/xlink' xlink:href='#csst_nav_svg-icon'></use>
</svg>
"
["tree_type"]=> array(3) {
[0] => string(9) "post_type"
[1] => string(8) "taxonomy"
[2] => string(6) "custom"
}
["db_fields"] => array(2) {
["parent"] => string(16) "menu_item_parent"
["id"] => string(5) "db_id"
}
["max_pages"] => int(1)
["has_children"] => bool(false)
}
["theme_location"] => string(0) ""
}
Of note is the walker
arg. You can see that it names our walker class, and even catches the SVG icon that we saved as a class member! The other items under the walker
arg are either unused or uninteresting for our purpose of customizing a nav menu.
$id
$id
seems to be a big disappointment. It’s always 0. Not even gonna dump it for you.
Practical Uses for the start_el()
Args
Let’s start with what core does in Walker_Nav_Menu -> start_el()
. As I noted above, they use $depth
to prepend tabs, seemingly in pursuit of more legible source code. Such craftsmanship! Also, you’d better believe they grab those CSS classes from $item
.
In my custom version, I have two value-adds. First, I have a chance to build the menu item according to my own coding preferences. I happen to hate ternary operators, for example. Second, I have a chance to namespace all of the CSS classes that WordPress generates for the menu item. current-menu-item
would become csst_nav-current_menu_item
. I do this by passing the css classes to a custom method which renames the classes and passes them back. They come back with the prefix for our project, and some more consistent formatting around things like hyphens and underscores.
That does it for start_el()
! I have nothing more to say about the opening HTML for a menu item. But now that it’s open, we’d better close it.
end_el( &$output, $item, $depth = 0, $args = array() )
The end_el()
is a very short method: All it does is append the closing html for a menu item. It carries the same args as start_el()
, except for $id
, which it omits. Also, &$output
will be larger than it was when we encountered it in start_el()
, since the current $item
has been appended to it. These args are var_dump()
‘d in my discussion of start_el()
, so I won’t go over them again.
As for practical usage, it’s interesting to note that core simply prints a closing li
. Instead, I’m reaching back into $args
in order to close the element with the markup I specified via the after
arg when creating our shortcode.
start_lvl( &$output, $depth = 0, $args = array() )
The purpose of this oddly named fellow is to start a new “level” in the structure we’re digging through. That sounds pretty abstract, but fortunately we have a very familiar example at our fingertips: In a nav menu, a new level is simply a submenu!
This method carries three parameters, &$output
, $depth
, and $args
, which are all var_dump()
‘d above. As for usage, core takes this opportunity to open a new ul
for the submenu, complete with indented source code. Very nice. However, many times I have found myself unhappy with the submenu treatment. For example, I want to add a toggle icon to indicate that there is a submenu. I want the submenu to use my markup and CSS classes. And, I want the submenu to respond as a show/hide when the toggle is clicked. This is the perfect time to make these customizations.
Good times: Our submenu is open and submenu items will be appended to it via start_el()
and end_el()
, above. If there are submenus inside of this submenu item, no problem. Those will be appended via start_lvl()
as well. Once that’s all done, we’ll need to close our submenu.
end_lvl( &$output, $depth = 0, $args = array() )
This method is very similar to end_el()
, only instead of closing a menu item, it closes a submenu. For core, that’s a closing ul
. For me, it’s a closing span
.
Other Elements
My custom walker does have some other elements: A constructor and a couple of attributes. I use the constructor to call my svg icon class and grab a toggle icon for the submenus. I save the icon as an attribute on the class, so my other methods can easily use it.
Core’s Walker_Nav_Menu
class has some other elements as well:
- A mysterious attribute called
$tree_type
, which even core does not use. The source documents it as “What the class handles”, and avar_dump()
gives us:<?php array(3) { [0]=> string(9) "post_type" [1]=> string(8) "taxonomy" [2]=> string(6) "custom" } ?>
Which, meh, whatever.
- An attribute called
$db_fields
, which is a bit opaque. Avar_dump()
gives us:<?php array(2) { ["parent"] => string(16) "menu_item_parent" ["id"] => string(5) "db_id" } ?>
To which, I yield. If you can figure out how these are used and how we might leverage them for something interesting, leave it in the comments!
Resources and Next Steps
Walker
and its heirs are not as heavily discussed or documented as other parts of WordPress, which is one of the things that inspired me to write this article. However, there is some prior work available. I first became interested in walker deep-dives when I saw this port of a BootStrap nav menu. And, predictably, the codex gives a couple of examples as well.
The main axe I’ve been grinding in this article has been to gain control of my class names and markup around nav items and submenus, but there are many other possibilities. Perhaps we could reach into $item
and grab the featured image or some post meta, if $item
happens to be linking to a post. If it happens to be linking to a term archive, perhaps we’d want to grab something from the upcoming term_meta system. You could even do something totally different, like output menu items with the markup and classes expected by your favorite jQueryUI widget or image slider. Give it a try and happy var_dump()
ing!
This guide covered a topic seldom seen on the net.