How to display the title for each menu item in a span attribute

I’m trying to add a CSS hover effect to my theme’s main menu and the effect is based on an attribute passed in a <span> element. The problem is that I am unable to produce this dynamically with the title of each menu item. Here is the HTML that I would like to produce:

<nav>
    <a href="#"><span data-hover="Home">Home</span></a>
    <a href="#"><span data-hover="About">About</span></a>
    <a href="#"><span data-hover="Contact">Contact</span></a>
</nav>

Now in WP I tried adding the link_before and link_after attribute in order to add the <span> element but I’m not sure how to produce the title for each menu item for the “data-hover” attribute. Any help would be highly appreciated.

Related posts

2 comments

  1. When calling the wp_nav_menu() function you can pass the <span> tags in the link_before y link_after arguments:

    wp_nav_menu(array('link_before' = > '<span>', 'link_after' => '</span>'));
    

    To have differents attributes in the span for each menu item you may need to use a custom nav_walker. For example, the next custom Nav_Walker is the default from WordPress, I’ve just modified it to add the <span> with the data-hover attribute:

    //First, define the callback when displaying the menu
    <nav>
        <?php wp_nav_menu( array( 
                'container'       => '',
                'container_class' => '',
                    'theme_location'  => 'topmenu',
                'menu_class'      => 'nav',
                    'fallback_cb'      => '',
                'walker' => new Custom_Walker(),
                         ) );
    ?>
    

    The custom nav walker, in this case Custom_Walker must be in functions.php or included in it:

    class Custom_Walker extends Walker_Nav_Menu {
    /**
     * @see Walker::$tree_type
     * @since 3.0.0
     * @var string
     */
    var $tree_type = array( 'post_type', 'taxonomy', 'custom' );
    
    /**
     * @see Walker::$db_fields
     * @since 3.0.0
     * @todo Decouple this.
     * @var array
     */
    var $db_fields = array( 'parent' => 'menu_item_parent', 'id' => 'db_id' );
    
    /**
     * @see Walker::start_lvl()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param int $depth Depth of page. Used for padding.
     */
    function start_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat("t", $depth);
        $output .= "n$indent<ul class="sub-menu">n";
    }
    
    /**
     * @see Walker::end_lvl()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param int $depth Depth of page. Used for padding.
     */
    function end_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat("t", $depth);
        $output .= "$indent</ul>n";
    }
    
    /**
     * @see Walker::start_el()
     * @since 3.0.0
     *
     * @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. Used for padding.
     * @param int $current_page Menu item ID.
     * @param object $args
     */
    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
    
        $class_names = $value = '';
    
        $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 ) . '"' : '';
    
        $output .= $indent . '<li' . $id . $value . $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 );
    
        $attributes = '';
        foreach ( $atts as $attr => $value ) {
            if ( ! empty( $value ) ) {
                $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
                $attributes .= ' ' . $attr . '="' . $value . '"';
            }
        }
    
        $item_output  = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before;
                // HERE are your <span> tags
                $item_output .= '<span data-hover="'.esc_attr($item->title).'">';
                $item_output .= apply_filters( 'the_title', $item->title, $item->ID );
                $item_output .= '</span>';
                $item_output .= $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;
    
        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }
    
    /**
     * @see Walker::end_el()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param object $item Page data object. Not used.
     * @param int $depth Depth of page. Not Used.
     */
    function end_el( &$output, $item, $depth = 0, $args = array() ) {
        $output .= "</li>n";
    }
    }
    

    Or maybe better (or not, I’m not sure because it needs a query foreach menu item via the get_post() function if we want to run the filter only for the title of menus and not to all post titles), use the_title filter hook (see toscho answer for a better way to use the_title filter hook if you decide to use this filter instead the custom nav-walker):

    function add_span_to_menu($title, $id) {
    
        //Run this function only for nav_menu_item post types
        $item = get_post( $id );
        if($item->post_type != 'nav_menu_item') return;
    
        $title = '<span data-hover="'.esc_attr($title).'">'.$title.'</span>';
    
        return $title;
    
    }
    add_filter('the_title', 'add_span_to_menu', 10, 2);
    
  2. You can filter the nav menu item’s title with the_title.

    // from wp-includes/nav-menu-template.php, Walker_Nav_Menu::start_el()
    $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
    

    Unfortunately, this filter is used on many other places too, so you have to turn on the filter as late as possible, and you should turn it off when the items have been parsed.

    Sample, code, not tested:

    // turn it on
    add_filter( 'wp_nav_menu_objects', function( $items )
    {
        add_filter( 'the_title', 'change_nav_title' );
        return $items;
    });
    // turn it off
    add_filter( 'wp_nav_menu', function( $nav_menu )
    {
        remove_filter( 'the_title', 'change_nav_title' );
        return $nav_menu;
    });
    
    // change the title
    function change_nav_title( $title )
    {
        return sprintf(
            '<span data-hover="%1$s">%2$s</span>',
            esc_attr( $title ),
            $title
        );
    }
    

Comments are closed.