Function Friday #11: displaying menu item descriptions with a custom walker

Function Friday #11

Every Friday, I’m sharing code snippets that I use to customize WordPress. Feedback/suggestions are always welcome! For more information, check out the first post in the series.


Have you ever wondered about the “Description” field on the Appearance → Menus page, with its notice that “The description will be displayed in the menu if the current theme supports it“?

I’d seen it before (when enabling the “CSS Classes” field via Screen Options) but hadn’t paid much attention to it, until I had a client that wanted some text to appear next to each item in the navigation menu.

A quick Google search introduced me to the Walker class, and just like with my first look at the Widgets API, I wasn’t sure where to start with all that code – was it really necessary just to tack on a little bit of text?

It turns out you don’t have to modify much of it to get a neat custom menu like this:

Menu with description

The code

First you have to extend the part of the Walker_Nav_Menu class that affects the element’s (i.e. the menu item’s) output, start_el:

// Create custom walker to show menu item descriptions
class Nav_Menu_With_Description extends Walker_Nav_Menu {
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
            $t = '';
            $n = '';
        } else {
            $t = "\t";
            $n = "\n";
        }
        $indent = ( $depth ) ? str_repeat( $t, $depth ) : '';

        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;

        $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );

        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

        $output .= $indent . '<li' . $id . $class_names .'>';

        $atts = array();
        $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
        $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
        $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
        $atts['href']   = ! empty( $item->url )        ? $item->url        : '';

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

        $attributes = '';
        foreach ( $atts as $attr => $value ) {
            if ( ! empty( $value ) ) {
                $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
                $attributes .= ' ' . $attr . '="' . $value . '"';
            }
        }

        /** This filter is documented in wp-includes/post-template.php */
        $title = apply_filters( 'the_title', $item->title, $item->ID );

        /**
         * Filters a menu item's title.
         *
         * @since 4.4.0
         *
         * @param string   $title The menu item's title.
         * @param WP_Post  $item  The current menu item.
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before . $title . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;

        // If there is a description, display it
        if ( $item->description ) {
            $item_output .= '<br />' . $item->description;
        }

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }
}

This looks like a ton of code, but the majority is copied directly from Walker_Nav_Menu, the class we’re extending. I’ve removed the commented sections for the sake of brevity.

The relevant changes are in the first line, in which we name our custom walker (I’ve named it Nav_Menu_With_Description, but if you’re creating the walker to do something else you’d obviously want to name it accordingly):

class Nav_Menu_With_Description extends Walker_Nav_Menu {

Then all of the other code is identical to Walker_Nav_Menu, except the part that affects the menu item title:

/**
 * Filters a menu item's title.
 *
 * @since 4.4.0
 *
 * @param string   $title The menu item's title.
 * @param WP_Post  $item  The current menu item.
 * @param stdClass $args  An object of wp_nav_menu() arguments.
 * @param int      $depth Depth of menu item. Used for padding.
 */
$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;

// If there is a description, display it
if ( $item->description ) {
    $item_output .= '<br />' . $item->description;
}

The new code is the last few lines, in bold. It checks whether the item has a “description” value, and if it does, it stores a linebreak and the value in the item_output variable, after the menu item link.

You can now use the custom walker you’ve created with the wp_nav_menu function:

<?php wp_nav_menu( array( 'theme_location' => 'top', 'walker' => new Nav_Menu_With_Description ) ); ?>

Or you can automatically apply the custom walker to a specific menu location, by hooking into wp_nav_menu_args:

// Always use custom walker on Top Menu location
function drollic_custom_nav_args( $args ) {
    if ( 'top' == $args['theme_location'] ) {
        $args['walker'] = new Nav_Menu_With_Description();
    }
    return $args;
}
add_filter('wp_nav_menu_args', 'drollic_custom_nav_args');

Where does it go?

The menu locations are set in your theme, so I think this code belongs with your theme as well, although you could also make a case for altering how menus display with a functionality plugin. Read more about both options in the first post of this series.

Resources

Leave a Reply

Your email address will not be published. Required fields are marked *