Animated navigation items using jQuery

By John Faulds

Dave Shea recently published an article on A List Apart (ALA), CSS Sprites2 – It's JavaScript Time', about how to use jQuery to create the effect of animated rollovers on navigation items.

The technique he outlines makes use of the same image replacement method as outlined in ALA's original Sprites article. The problem with this method however is that it uses a large negative text-indent to remove the default text from screen, and with images turned off in the browser, you don't see anything. This has accessibility implications not only from the perspective of those with disabilities, but also for those who deliberately turn images off, i.e. people on slower connections or those using handheld devices who are trying to limit the amount of information downloaded to their phone.

When I do use image replacement, I prefer a method which leaves the text on screen when images are turned off – the Gilder Levin Ryznar Jacoubsen IR method, which I've written about before. Having been using jQuery quite a bit myself lately, I thought I'd see if I could come up with a similar implementation which would let me use the image replacement method I prefer.

Here's my working example.

The mark-up is essentially the same as that Dave has used:

<ul id="nav">
    <li id="n-home"><a href="#home"><em></em>Home</a></li>
    <li id="n-about"><a href="#about"><em></em>About</a></li>
    <li id="n-services"><a href="#services"><em></em>Services</a></li>
    <li id="n-contact"><a href="#contact"><em></em>Contact</a></li>
</ul>

except that I'm using IDs on my list items and rather than having a class on the <ul> to identify the current page, I've got an ID on the body. The CSS looks like this:

#nav {
      width: 401px;
      height: 48px;
      margin: 0;
      padding: 0;
      background: url(images/animated-nav.png) repeat-x;
      list-style: none;
      overflow: hidden
  }

  #nav li {
      position: absolute;
      overflow: hidden;
      font-size: 1em;
  }

  #nav li, #nav li * { height: 48px }
  #nav a { display: block }
  #nav em, #nav span {
      display: block;
      position: absolute;
      top: 0; left: 0;
      z-index: 1;
      background: url(images/animated-nav.png) no-repeat;
      cursor: pointer;
  }

  #nav span { display: none }

  #n-home { left: 23px }
  #n-home, #n-home * { width: 77px }
  #n-home em { background-position: -23px 0 }
  #n-home:hover em, #n-home span, #home #n-home em { background-position: -23px -49px }
  #n-about, #n-about * { width: 83px }
  #n-about { left: 99px }
  #n-about em { background-position: -99px 0 }
  #n-about:hover em, #n-about span, #about #n-about em { background-position: -99px -49px }
  #n-services, #n-services * { width: 98px }
  #n-services { left: 182px }
  #n-services em { background-position: -182px 0 }
  #n-services:hover em, #n-services span, #services #n-services em { background-position: -182px -49px }
  #n-contact, #n-contact * { width: 98px }
  #n-contact { left: 280px }
  #n-contact em { background-position: -280px 0 }
  #n-contact:hover em, #n-contact span, #contact #n-contact em { background-position: -280px -49px }

  #nav .over { text-indent: -999em }
  #nav .over em { background-image: none }

The only additional rules added to get the animated effect to work are:

#nav span { display: none }
#nav .over { text-indent: -999em }
#nav .over em { background-image: none }

And the javascript:

$(document).ready(function(){
    // Get the ID of the body
    var parentID = $("body").attr("id");
    // Loop through the nav list items
    $("#nav li").each(function() {
        // compare IDs of the body and list-items
        var myID = $(this).attr("id");
        // only perform the change on hover if the IDs don't match (so the active link doesn't change on hover)
        if (myID != "n-" + parentID) {
            // for mouse actions
            $(this).children("a").hover(function() {
                // add a class to the list item so that additional styling can be applied to the <em> and the text
                $(this).addClass('over');
                // add in the span that will be faded in and out
                $(this).append("<span></span>");
                $(this).find("span").fadeIn(400);
            }, function() {
                $(this).removeClass('over');
                // fade out the span then remove it completely to prevent the animations from continuing to run if you move over different items quickly
                $(this).find("span").fadeOut(400, function() {
                    $(this).remove();
                });
            });
            // for keyboard actions
            $(this).children("a").focus(function() {
            	if ($(this).attr('class')!='over') {
                $(this).addClass('over');
                $(this).append("<span></span>");
                $(this).find("span").fadeIn(400);
              }
            });
            $(this).children("a").blur(function() {
                $(this).removeClass('over');
                $(this).find("span").fadeOut(400, function() {
                    $(this).remove();
                });
            });
        }
    });
});

Essentially, what's happening is that javascript is being used to append an empty <span> to each anchor which is set to display: none by the CSS on page load, and when the mouse hovers over it, the <span> is animated to fade in.

Except that it's not quite that simple. Because there's already an empty <em> in each anchor which handles the rollover effect if javascript isn't available, and because the change of the background-image on hover happens immediately, you don't really notice the fading in of the hover state of the image because it's fading in over the top of an image of itself.

So what I needed to do was to remove the background-image from the <em> when each anchor was hovered over – #nav .over em { background-image: none }. But this would then leave the text of each link visible, so I also needed to use text-indent to remove it from view – #nav .over { text-indent: -999em }. Using a negative text-indent was one of the reasons I shied away from the technique used in the ALA article, but at least in this example, the text is still visible in its normal state if images are turned off and only disappears when hovered over.

The javascript for this example is a bit lighter than the ALA example and it ticks a couple more accessibility boxes in that the animated effect will work for both mouse and keyboard-only users with both :hover and :focus events, and the text of the links is still visible with images turned off, but as mentioned above, it does still need to use text-indent: -999em which I would've preferred not to have used. So if anyone has any ideas about how to remove the need for text-indent, I'd like to hear them.

Update

I noticed recently that Firefox has a problem when clicking the links in that it was layering a second empty span over the top of the first one applied by the part of the script that produces the effect for when the link receives focus only rather than hover. The second empty span was preventing the browser from redirecting to the anchor destination. So I've modified the script to check whether the class of over has already been applied when the link receives focus and only apply the class and the extra span if it hasn't been.