1

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.

2 Answers 2

0

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);
1
  • Your suggestion with a bit of tweaking did the trick. Thanks! ;-)
    – Daem0n
    Sep 20, 2013 at 17:54
3

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
    );
}
4
  • I had the_title filter in my answer too (at the end). In your answer the filter will run for every post title. In my answer I added the code to check if the current item is a nav_menu_item (post type) but I'm not sure of the performance implications of such checking. What do you think?
    – cybmeta
    Sep 20, 2013 at 16:42
  • 1
    @cybnet No, it runs on nav menu items only. That’s the whole point of the de-registration.
    – fuxia
    Sep 20, 2013 at 16:44
  • Great lesson for me!!!
    – cybmeta
    Sep 20, 2013 at 16:45
  • Best and Powerful one
    – mlimon
    Mar 7, 2018 at 11:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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