Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

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.

share|improve this question

2 Answers 2

up vote 0 down vote accepted

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);
share|improve this answer
    
Your suggestion with a bit of tweaking did the trick. Thanks! ;-) –  Daem0n Sep 20 '13 at 17:54

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
    );
}
share|improve this answer
    
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 '13 at 16:42
1  
@cybnet No, it runs on nav menu items only. That’s the whole point of the de-registration. –  toscho Sep 20 '13 at 16:44
    
Great lesson for me!!! –  cybmeta Sep 20 '13 at 16:45

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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