A List Apart

Menu

CSS Sprites2 - It's JavaScript Time

Issue № 266

CSS Sprites2 - It’s JavaScript Time

by Published in CSS, HTML, JavaScript · 86 Comments

A sense of movement is often the differentiator between Flash-heavy web sites and standards-based sites. Flash interfaces have always seemed more alive—responding to the user’s interactions in a dynamic way that standards-based web sites haven’t been able to replicate.

Lately that’s been changing, of course, with a resurgence in dynamic interface effects, helped along by JavaScript libraries that make it easy—libraries such as Prototype, Scriptaculous, Moo, YUI, MochiKit (and I could go on). It’s high time to revisit the CSS Sprites technique from four years ago, and see if we can’t interject a little bit of movement of our own.

The examples below demonstrate inline CSS Sprites2, the technique we’ll be covering in this article:

Enter the jQuery

So here’s the first caveat: we’re going to lean on jQuery to make this happen. jQuery is a maturing JavaScript library that does the same neat stuff as all the other JavaScript libraries, and it has an additional advantage that lends itself particularly well to extending CSS Sprites: jQuery allows us to select elements on a page using a CSS-like syntax that we already know.

We must note the non-trivial extra kilobytes that the library will add to your initial page loads. An external JavaScript file is cacheable, of course, so it’s a one-time-only hit the first time a user comes to your site. The most compact version of jQuery weighs in at 15k. It’s unavoidable overhead, and that may be a cause for concern. If you’re already using jQuery on your site for other purposes, the overhead is a non-issue. If you’re interested in adding it solely for this technique, consider the file size and decide for yourself whether the effect is worth it. (Since Google is now hosting jQuery, you could link to their version of the library, as we do in these examples, and hope that many of your users will already have that URL cached in their browser.)

As for other JavaScript libraries? There’s absolutely no reason why you couldn’t or shouldn’t use them; consider this article an open invitation to port this technique to your library of choice—and link to your port in the comments.

Basic HTML and CSS setup

The first thing we want to do is create a default, unscripted state for users without JavaScript. (We read Jeremy Keith’s article from a few years back and are now big fans of unobtrusive DOM scripting, naturally.)

We already have a CSS-only method for rollovers, so let’s begin by building our navigation to function with a basic CSS Sprites setup. And because we’re lazy, we won’t build the rollovers a second time, we’ll just reuse this foundation later and add the jQuery on top of it. We’ll get to that in a bit.

I’ll leave the hows and whys of CSS Sprites to the original article, but there are a few things to clarify below. Let’s start with the HTML. Pay close attention to this structure—we’ll refer back to it a lot:

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

Each class serves a purpose: the containing ul has a class of nav that allows us to target it in our CSS (and later JavaScript,) as well as a second class of .current-about that we’ll use to indicate, within the navigation, which page or section of the site we’re currently viewing. Each li element has its own unique class, which we’ll also use for targeting.

So far so good. Our navigation’s markup is a simple and accessible HTML list, and we have enough classes to get going with our Sprites:

.nav {
    width: 401px;
    height: 48px;
    background: url(../i/blue-nav.gif) no-repeat;
    position: absolute;
    top: 100px;
    left: 100px;
}

We’ve set the position value to absolute to change the positioning offset to the li, instead of the body element. We could have used relative instead, to accomplish the same thing while leaving the .nav element within the document flow. There are reasons to do it either way, but for now we’ll stick with absolute. For more on this absolute/relative tossup, see Douglas Bowman’s article on the subject.

The meat of our Sprites technique is in applying a background image to each of the nav items, and absolutely positioning them within the parent ul:

.nav li a:link, .nav li a:visited {
    position: absolute;
    top: 0;
    height: 48px;
    text-indent: -9000px;
    overflow: hidden;
}
    .nav .home a:link, .nav .home a:visited {
        left: 23px;
        width: 76px;
    }
    .nav .home a:hover, .nav .home a:focus {
        background: url(../i/blue-nav.gif) no-repeat -23px -49px;
    }
    .nav .home a:active {
        background: url(../i/blue-nav.gif) no-repeat -23px -98px;
    }

We’re going a bit further than the original article and defining :focus and :active states. The former is a minor addition to trigger the hover image when an anchor is subject to either the :hover or :focus states. The latter adds a new state when the user clicks on a nav item. Neither is essential, although it’s a good idea to define them both. The overflow: hidden; rule is new too—it’s just there to prevent some browsers from extending a dotted outline from the element’s position all the way off the left side of the screen to the negatively-indented text.

Example 1: Basic CSS Sprites setup.

That gets us to our real starting point—a working Sprite-enabled navigation menu, complete with currently-selected navigation items. And now to extend it.

Initializing in jQuery

Note that everything below will be placed inside a jQuery function that ensures the code runs only once the document is fully loaded. The code snippets you see below all assume they’re running inside this function, so if you encounter errors, remember to check their placement:

$(document).ready(function(){    // everything goes here});

Since the Sprite menu is our fallback state when JavaScript is disabled (if we’ve done it right), we’d better get rid of those CSS-applied background images on hover, because we’ll create our own in the script below:

$(".nav").children("li").each(function() {
    $(this).children("a").css({backgroundImage:"none"});
});    

On the first line, we’re querying any elements with a class of nav and attaching a new function to each child li element they contain. That function is on the second line, and it queries the this object for any a child elements. If it finds them, it sets the CSS background-image property to none. Given the context, this means the li elements that the function is running on.

Example 2: Disabling CSS hovers with jQuery.

That works…but we also lose our currently-selected item in the process. So we need to throw in a check to see which item we’ve identified with the current-(whatever) class we applied to the parent ul, and skip that one from our background image removal. The previous snippet needs to be expanded a bit:

$(".nav").children("li").each(function() {
    var current = "nav current-" + ($(this).attr("class"));
    var parentClass = $(".nav").attr("class");
    if (parentClass != current) {
        $(this).children("a").css({backgroundImage:"none"});
    }
});    

The second line now creates a variable called current that uses each li’s class in sequence to create a string that should match the parent ul’s classes, if that particular li is the currently-selected item. The third line creates a second variable which reads the actual value directly from the ul. Finally, the fourth line compares the two. If they don’t match, only then do we set the background-image property of the a element. This skips the change in background image for the currently-selected item, which is exactly what we’re shooting for.

Attaching the events

Now we need to attach a function to each of the li elements for every interaction event we want to style. Let’s create a function for this, called attachNavEvents:

function attachNavEvents(parent, myClass) {
    $(parent + " ." + myClass).mouseover(function() {
        // do things here
    }).mouseout(function() {
        // do things here
    }).mousedown(function() {
        // do things here
    }).mouseup(function() {
        // do things here
    });
}

This function takes two arguments. The first is a string containing the literal class of the parent element, complete with preceding period, as you’ll see when we call it below. The second is a string containing the class of the particular li to which we’re attaching the events. We’re going to combine both of those on the first line of the function to create a jQuery selector that targets the same element as the CSS descendant selector of, for example, .nav .home. (Which element depends on the arguments we’ve passed to the function, of course.)

Because jQuery allows us to chain multiple functions on a single object, we’re able to create all the event-triggered functions at the same time. Chaining is a unique jQuery concept. It’s a bit tricky to wrap your mind around—it’s not essential to understand why this works, so if you’re confused, just take it for granted that it does work, for now.

Now we’ll attach these functions to every item in our navigation. The following is a verbose way—we’ll optimize it later—but for now, let’s run the function on every li. For arguments, we’ll pass the parent of each, as well as the li’s own class:

attachNavEvents(".nav", "home");
attachNavEvents(".nav", "about");
attachNavEvents(".nav", "services");
attachNavEvents(".nav", "contact");

This doesn’t do much yet, but we’re about to fix that.

Example 3: Basic script setup for events.

The theory

I’m going to explain what happens next up front. Stay with me—it’s important to understand what’s going on here because you’ll need to style the elements we’re manipulating.

For each of the links, we’ll create a brand new div element inside the li we’re targeting, which we’ll use for our jQuery effects. We’ll apply the nav image to this new div using the same background-image rule we used for the a element inside the shared parent li. We’ll also absolutely position the div within the parent. It’s more or less a duplicate of the existing a element in our CSS Sprites setup. Through trial and error, I’ve found that this new div creation proves less glitchy than directly applying the jQuery effect to the existing elements—so it’s a necessary step.

The style for this div must already exist in the CSS. We’ll create a new class for this li (.nav-home), based on the class of the targeted li (so it doesn’t conflict with anything else we’ve created so far), and add the style:

.nav-home {
    position: absolute;
    top: 0;
    left: 23px;
    width: 76px;
    height: 48px;
    background: url(../i/blue-nav.gif) no-repeat -23px -49px;
}

The practice

Now it’s time to add the effects. When the mouseover event is triggered, we’ll create the div element and give it the previously-mentioned class. We need it to start out invisible before fading in, so we’ll use the jQuery css function to set a CSS display value of none. Finally we’ll use the jQuery fadeIn function to fade it from hidden to visible, and pass an argument of 200 to that function to specify the duration of this animation in milliseconds (line wraps marked » —Ed.):

function attachNavEvents(parent, myClass) {
    $(parent + " ." + myClass).mouseover(function() {
        $(this).before('');
        $("div.nav-" + myClass).css({display:"none"}) »
        .fadeIn(200);
    });
}

Then, we perform the same in reverse on the mouseout event—we’ll fade out the div. Once it’s finished fading, we’ll clean up after ourselves by removing it from the DOM. This is how our attachNavEvents function should look:

function attachNavEvents(parent, myClass) {
    $(parent + " ." + myClass).mouseover(function() {
        $(this).before('');
        $("div.nav-" + myClass).css({display:"none"}) »
        .fadeIn(200);
    }).mouseout(function() {
        // fade out & destroy pseudo-link
        $("div.nav-" + myClass).fadeOut(200, function() {
            $(this).remove();
        });
    });
}

And that’s pretty much it for the hovers:

Example 4: Scripted hover events.

We’d better do something about the mousedown and mouseup events too, if we had previously defined a change for the :active state in the CSS. We’ll need a different class from the hovers so we can target it uniquely in the CSS, so let’s change the class on mousedown. We’ll also want to revert it on mouseup to restore the :hover state, since the user may not have moved their mouse away from the nav item. Here’s what the revised attachNavEvents function now looks like:

function attachNavEvents(parent, myClass) {
    $(parent + " ." + myClass).mouseover(function() {
        $(this).before('');
        $("div.nav-" + myClass).css({display:"none"})»
        .fadeIn(200);
    }).mouseout(function() {
        $("div.nav-" + myClass).fadeOut(200, function() {
            $(this).remove();
        });
    }).mousedown(function() {
        $("div.nav-" + myClass).attr("class", "nav-" »
        + myClass + "-click");
    }).mouseup(function() {
        $("div.nav-" + myClass + "-click").attr("class", »
        "nav-" + myClass);
    });
}

We can reuse the hover div style, by slightly modifying the background position to adjust which part of our main Sprite image is showing on click:

.nav-home, .nav-home-click {
    position: absolute;
    top: 0;
    left: 23px;    width: 76px;
    height: 48px;
    background: url(../i/blue-nav.gif) no-repeat -23px -49px;
}
.nav-home-click {
    background: url(../i/blue-nav.gif) no-repeat -23px -98px;
}

Now we’ve got the hovers, the currently-selected nav item, and click events all worked out:

Example 5: Putting it all together.

Other considerations

We’re not limited to the fade effect either. jQuery has a built-in slideUp/slideDown function we can use as well (which is shown in the second example at the top of this article). Or, we can get really fancy and create custom CSS-defined animation effects using the jQuery animate function (as shown in the third example). A word of caution about animate—the results can be a touch erratic, as you may have noticed in the example.

Cross-browser functionality is a bit of a freebie; jQuery works across most modern browsers, so everything you see here works in IE6+, Firefox, Safari, Opera, etc. We’ve also accounted for multiple graceful degradation scenarios. If a user has JavaScript turned off, they get basic CSS Sprites. If they’ve disabled JavaScript and CSS, they get a basic HTML list. And, we get the other benefits of CSS Sprites too, since we’re still using a single image for all the various navigation states and effects.

Though it’s not required, it’s strongly suggested that you embrace subtlety; animation speeds of more than a few hundred milliseconds may be fun to begin with, but they will quickly grate on the nerves of those who use the site you’re building after the novelty wears off. Err on the side of quicker animation speeds, rather than slower.

One potential glitch you might run into is when other text on the page seemingly “flashes” during animations. This is a complicated issue that has to do with sub-pixel rendering common in modern operating systems, and the best fix seems to be to apply a just-slightly-less-than-opaque opacity value to force a particular text rendering mode. If you add this to your CSS, the flashing should clear up at the expense of regular anti-aliased text instead of sub-pixel anti-aliased text:

p {
    opacity 0.9999;
}

In the demos this is applied to p, but that caused a conflict with the A List Apart CMS. But we can apply this rule to any element on the page, so let’s pick something innocuous.

Packaged to go

You don’t actually need to remember any script in this article, since there’s a pre-built function awaiting you in this final example. Using the JavaScript in the HTML file as a reference, you only need to edit a single line of JavaScript to apply Sprites2 to your site:

$(document).ready(function(){
    generateSprites(".nav", "current-", true, 150, "slide");
});

The generateSprites function takes five arguments:

  1. The primary class of your parent ul, including the period.
  2. The prefix you’re using for selected items, e.g., for a selected class of selected-about, use selected- as the value.
  3. A toggle to indicate whether you’re styling the :active state. Set it to true if you’ve defined the :active state and jQuery equivalents in your CSS, otherwise set it to false.
  4. The animation speed, in milliseconds. e.g., 300 = 0.3 seconds.
  5. Your preferred animation style, as a string. Set to “slide” or “fade”, it defaults to the latter.

Example 6: One easy line of script to modify, thanks to the pre-built function.

You will still need to position and style the various scripted elements in your CSS, so feel free to use the CSS file examples in this article as a reference.

Footnotes

During the writing of this article, a similar technique was written up elsewhere, albeit without our nice CSS Sprites fallback. We also discovered a very different animated jQuery menu that you may find useful.

86 Reader Comments

Load Comments