Mint Digital

Mint Digital

Cutting down on CSS sprites

Posted in by Ron DeVera

09 December, 2009

When making a web page load faster, one of the most commonly cited front-end techniques is minimizing the number of HTTP requests. In fact, it's Yahoo!'s number one rule. Each HTTP request is slow; making many requests is very slow.

Our homepage is a collection of image-based links. Each link shows one image as its normal state, and another image as its hover state. The easy, obvious implementation is to have two image files per link.

However, this page had eighteen image-based links, which meant thirty-six separate image files. That means thirty-six HTTP requests—that's way too many.

The most popular technique for managing image-based links is CSS sprites. Rather than having two separate image files per link, instead combine the two states into a single image file, and use CSS to display the appropriate half when the user hovers over that link. This technique can cut your number of image files—and HTTP requests—in half.

When we built the second version of our homepage, we used CSS sprites for each section—the links to our portfolio, Twitter account, contact info, and more. Each link used a sprite image like this:

Whenever someone loaded that page, with its eighteen image-based links, the browser made about eighteen HTTP requests to fetch the required sprite images. Half as many requests as before, but we still could do better.

The latest version of our homepage features not only a refreshed design, but a complete CSS rewrite. It still has eighteen separate links, and still uses the CSS sprite technique, but with only two images. Shiny. But how?

Here are the two images we use:

Our technique is conceptually similar to image maps, in which you use coordinates to define clickable areas of an image. However, unlike plain old image maps, we have semantic HTML and attractive hover states.

The HTML is structured like this:

<div class="about">
  <p id="about-methodology">
    <a href="/about/methodology">Real life is complex.
      Good software is simple. Our methodology bridges
      the gap</a>
  </p>
  <p id="about-blog">
    <a href="/blog">Mint blog</a>
  </p>
  <p id="about-people">
    <a href="/people">Mint is a mix of software
      developers, interaction designs, and ideas
      people</a>
  </p>
  <!-- ...more links... -->
</div>

Note that, even though the page is image-based, the HTML is semantic so that the page remains readable even if CSS fails to load, or if you're using a screen reader.

In the CSS, we first set the wrapper to use the normal state image as its background.

div.about {
  position:   relative;
  width:      741px; /* Width of about.gif */
  height:     681px; /* Height of about.gif */
  margin:     0 auto;
  background: transparent url(/images/about/about.gif)
                0 0 no-repeat;
}

Next, we set each link to use the hover state image as its background.

div.about a {
  background:   transparent
                  url(/images/about/about-hover.gif)
                  9999em 9999em no-repeat;
  text-indent:  -9999em;
}

That's it for images—just these two blocks. Every link uses about-hover.gif, so the browser can reuse it for each link without re-requesting it from the server.

Note that every link is invisible by default: each link's background is positioned far beyond its bottom right corner (background-position: 9999em 9999em), and its text is positioned far beyond its left edge (text-indent: -9999em).

Now, each paragraph must be positioned. First, some global properties:

div.about p {
  position: absolute;
  margin:   0;
  padding:  0;
}

Next, we position each individual paragraph to line up with its clickable area. Here's an example for p#about-methodology:

p#about-methodology {
  left:   180px;
  top:    326px;
  width:  337px;
  height: 90px;
}

To make sure that the paragraph is positioned correctly, it's helpful to temporarily add debug code like outline: 1px solid #0f0 to the CSS, which looks like this:

Let's revisit the global CSS for each link. We want each link to fill the entire area of its parent paragraph, so we extend it to this:

div.about a {
  display:      block; /* Fills parent's width */
  height:       100%;  /* Fills parent's height */
  overflow:     hidden;
  background:   transparent
                  url(/images/about/about-hover.gif)
                  9999em 9999em no-repeat;
  text-indent:  -9999em;
}

Lastly, we want to define the hover state for each link. So, we extend the CSS for p#about-methodology to this:

p#about-methodology {
  left:   180px;
  top:    326px;
  width:  337px;
  height: 90px;
}
p#about-methodology a:hover,
p#about-methodology a:focus,
p#about-methodology a:active {
  background-position: -180px -326px;
}

The background-position value for the link is simply the negative of its paragraph's position. This shows exactly the portion of its background—the hover image—that's needed. Easy!

(N.B.: We could have merged about.gif and about-hover.gif to cut it down to only one HTTP request. However, this would have made it less convenient to set and maintain the background coordinates for each hover state.)

Here's an overview of the CSS:

/* For the whole section: */
div.about {
  position:   relative;
  width:      741px; /* Width of about.gif */
  height:     681px; /* Height of about.gif */
  margin:     0 auto;
  background: transparent
                url(/images/about/about.gif)
                0 0 no-repeat;
}
div.about p {
  position: absolute;
  margin:   0;
  padding:  0;
}
div.about a {
  display:      block;
  height:       100%;
  overflow:     hidden;
  background:   transparent
                  url(/images/about/about-hover.gif)
                  9999em 9999em no-repeat;
  text-indent:  -9999em;
}
/* For each link: */
p#about-methodology {
  left:   180px;
  top:    326px;
  width:  337px;
  height: 90px;
}
p#about-methodology a:hover,
p#about-methodology a:focus,
p#about-methodology a:active {
  background-position: -180px -326px;
}

From here, we just add another block of CSS to position each paragraph/link, similar to that of p#about-methodology.

In the end, instead of eighteen separate sprite images, we now use just two. We cut image HTTP requests by 89%. We're pretty pleased with the results all around.

Comments

  1. Keith Chu

    09 December, 2009

    Very nice job, team!

    One question though: why reference each of the sections in .about by ID? ".about .methodology" seems pretty clear. Unless it's a JS hook.

  2. Ron DeVera

    09 December, 2009

    Thanks Keith! We typically do avoid IDs on sites in development, since classes can be easily reused without code conflicts.

    Here, I'd say both approaches work well. I chose IDs because I didn't foresee any good reason to reuse class names, and wanted the code to reflect that expectation.

  3. Nathan Fisher

    31 March, 2010

    Looks good, have you considered using attribute selectors? Gets rid of the encapsulating paragraph tag, which often carry little semantic meaning. Works in many mobile browsers, only caveat is IE6, but what else is new. :(

    a[href="/about/methodology"] { background-position: -180px -326px; }

Add your comment

  1. Or sign in using these: