111

Normal Wordpress Menu looks like:

Home | Blog | About us | Contact

But I've seen many pages with descriptions under these links:

Home Page | Our Blogs | About us | Contact
....meet us...| read more| basic info| contact form

How to achieve this?

(I want it to be core function for all my themes, so no plugins please, I just want to know how it's done)

4 Answers 4

125

You need a custom walker for the nav menu.

Basically, you add a parameter 'walker' to the wp_nav_menu() options and call an instance of an enhanced class:

wp_nav_menu(
    array (
        'menu'            => 'main-menu',
        'container'       => FALSE,
        'container_id'    => FALSE,
        'menu_class'      => '',
        'menu_id'         => FALSE,
        'depth'           => 1,
        'walker'          => new Description_Walker
    )
);

The class Description_Walker extends Walker_Nav_Menu and changes the function start_el( &$output, $item, $depth, $args ) to look for $item->description.

A basic example:

/**
 * Create HTML list of nav menu items.
 * Replacement for the native Walker, using the description.
 *
 * @see    https://wordpress.stackexchange.com/q/14037/
 * @author fuxia
 */
class Description_Walker extends Walker_Nav_Menu
{
    /**
     * Start the element output.
     *
     * @param  string $output Passed by reference. Used to append additional content.
     * @param  object $item   Menu item data object.
     * @param  int $depth     Depth of menu item. May be used for padding.
     * @param  array|object $args    Additional strings. Actually always an 
                                     instance of stdClass. But this is WordPress.
     * @return void
     */
    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 )
    {
        $classes     = empty ( $item->classes ) ? array () : (array) $item->classes;

        $class_names = join(
            ' '
        ,   apply_filters(
                'nav_menu_css_class'
            ,   array_filter( $classes ), $item
            )
        );

        ! empty ( $class_names )
            and $class_names = ' class="'. esc_attr( $class_names ) . '"';

        $output .= "<li id='menu-item-$item->ID' $class_names>";

        $attributes  = '';

        ! empty( $item->attr_title )
            and $attributes .= ' title="'  . esc_attr( $item->attr_title ) .'"';
        ! empty( $item->target )
            and $attributes .= ' target="' . esc_attr( $item->target     ) .'"';
        ! empty( $item->xfn )
            and $attributes .= ' rel="'    . esc_attr( $item->xfn        ) .'"';
        ! empty( $item->url )
            and $attributes .= ' href="'   . esc_attr( $item->url        ) .'"';

        // insert description for top level elements only
        // you may change this
        $description = ( ! empty ( $item->description ) and 0 == $depth )
            ? '<small class="nav_desc">' . esc_attr( $item->description ) . '</small>' : '';

        $title = apply_filters( 'the_title', $item->title, $item->ID );

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

        // Since $output is called by reference we don't need to return anything.
        $output .= apply_filters(
            'walker_nav_menu_start_el'
        ,   $item_output
        ,   $item
        ,   $depth
        ,   $args
        );
    }
}

Or, alternatively as @nevvermind commented, you could inherit all the functionalities of the parent's start_el function and just append the description to $output:

function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) 
{
    parent::start_el( $output, $item, $depth, $args );
    $output .= sprintf( 
        '<i>%s</i>', 
        esc_html( $item->description ) 
    );
}

Sample output:

enter image description here

Now enable the description field in wp-admin/nav-menus.php to get the ability to edit this field. If you don’t WP just trashes your complete post content into it.

enter image description here

Further reading:

And that’s it.

5
  • 13
    If for you inheritance != rewrite the whole method, just keep the same name, try this: public function start_el(&$output, $item, $depth, $args) { parent::start_el($output, $item, $depth, $args); $output .= sprintf('<i>%s</i>', esc_html($item->description)); }
    – nevvermind
    Dec 14, 2011 at 13:15
  • 2
    @nevvermind You should at least check if the description has some content. ;) The position of the description in my sample code is just the the most simple way to illustrate the solution. If you need to get the description into the anchor, you have to re-build the whole function.
    – fuxia
    Dec 16, 2011 at 10:38
  • 1
    yes, you'd have to write the whole method, no doubt about it, but for people who need to (say...) append it, it might just save them a lot of head-aches. And this is all WP's fault. Arrrgh!
    – nevvermind
    Dec 16, 2011 at 14:47
  • Nice one and I've used it in this answer by modifying a bit, may be you can make it better if I missed something, thanks.
    – The Alpha
    Aug 28, 2012 at 14:24
  • What I actually needed was the wp_nav_menu, but I needed to change the 'container_class' parameter, to work for my particular use case, where I on some condition swapped the main menu for another one, but needed the classes to be consistent for css.
    – D. Dan
    Jan 25, 2018 at 15:15
42

Since WordPress 3.0, you don't need a custom walker anymore!

There is the walker_nav_menu_start_el filter, see https://developer.wordpress.org/reference/hooks/walker_nav_menu_start_el/

Example:

function add_description_to_menu($item_output, $item, $depth, $args) {

   if (strlen($item->description) > 0 ) {
      // append description after link
      $item_output .= sprintf('<span class="description">%s</span>', esc_html($item->description));
    
      // or.. insert description as last item inside the link ($item_output ends with "</a>{$args->after}")
      // $item_output = substr($item_output, 0, -strlen("</a>{$args->after}")) . sprintf('<span class="description">%s</span >', esc_html($item->description)) . "</a>{$args->after}";
   }   
   return $item_output;
}
add_filter('walker_nav_menu_start_el', 'add_description_to_menu', 10, 4);
1
  • 1
    Nice! I was using the nav walker solution by @toscho, but this is much cleaner and easier to maintain.This should be the accepted answer, much better practice.
    – Ronaldt
    Jul 8, 2015 at 10:29
8

This isn't better or worse than other suggestions; it's just different. It's short and sweet too.

Rather than using the description field as @toscho suggests, you could fill in the "Title" field on each menu item with the text you want, and then use this CSS:

.menu-item a:after { content: attr(title); }

It would also be easy to use jQuery to append it, but the text is ornamental enough that CSS seems appropriate.

0
2

You can also write a <span> element after the navigation label in menus and use the following CSS rule to change its display setting (it's inline by default):

span {display:block}
1
  • 2
    Well its a simple and easy solution but why use span if you make it block anyway? xhtml/html4 not allows block elements inside links, html5 however does, so just use div, and no need for any css! Mar 5, 2013 at 21:02

Not the answer you're looking for? Browse other questions tagged or ask your own question.