B.

Automating CSS sprites for large organisations

CSS sprites have come a long way in the ten years since Dave Shea first wrote about them for A List Apart, way back in 2004.

An example of an early Amazon sprite Amazon was an early proponent of CSS sprites.

A CSS sprite is the technique of combining multiple images into a single image, and selectively displaying only parts of that image using the CSS background-position property. It was initially used mostly to make :hover states load quicker than the then commonly-used JavaScript onmouseover() equivalent. But it quickly became common practice for sites to bundle all of their icons and decorative images into a single optimised “sprite”. As well as faster rollovers, it also improved site performance, since downloading a single sprite is almost always faster than downloading each individual image separately.

The drawback with using CSS sprites was the extra work involved in laboriously measuring and transcribing the coordinates for each different element of your sprite. For each icon you needed to know its position within the sprite, and often its width and height as well. This also meant that removing individual icons was a painful process, as any change to the layout of the sprite meant recalculating all of those numbers.

While this approach might have been sustainable when developing a relatively small site on your own, it doesn’t really scale up when working in large teams. At Booking.com we have dozens of designers working across many different parts of the site, so we need a solution that is as resistant to mistakes as possible. We need automation.

Automation

In the last couple of years there have been incredible leaps forward in automating many of the common tasks that web developers have been used to doing by hand. We can now choose from ready-rolled templates like HTML5 Boilerplate and Rock Hammer. Or we can use entire UI libraries such as Twitter Bootstrap or Zurb Foundation. And we even have CSS pre-processors like LESS and SASS. This move towards automation culminated in task runners such as Grunt and newcomer Gulp which allow developers to write and run very simple tasks to automate away much of the boring, repetitive parts of their job.

For managing CSS sprites, there are several Grunt tasks out there that we could choose from. Some of the most popular include:

Most of the sprite tools share many of the same configuration options, allowing you to specify source and destination folders, CSS class names, the space you want to leave between images, and the packing algorithm to use. Some of the more advanced ones offer the ability to output both SVG and PNG sprites, and @2x retina-ready sprites. The CSS output can often be specified as plain CSS, or in LESS or SASS format.

When considering a solution that would be useful for a large team, we had a few specific requirements in mind:

  • It must work if we only have PNG files as input since not all web designers are comfortable working with SVG files, or have a license for a vector-graphic application
  • It must enable automating pseudo-classes like :hover and :active
  • It must be possible to integrate into our existing build system

While Grunt tasks are fun to play with, none of the ones we looked at satisfied all of our requirements.

Glue

Glue is a command-line only tool that is highly configurable and offers all of the features we were looking for. It accepts a folder or multiple sub-folders of PNG files as input. Generating pseudo-classes is handled through file-naming. For example, if you have two files named “foo.png” and “foo__hover.png”, the generated CSS will contain the :hover rule for your .foo class.

The default settings for Glue take a source directory full of images and outputs a sprite and a set of CSS rules based on the file names of the icons:

.sprite-source-foo, .sprite-source-bar, .sprite-source-baz {
  background-image:url('source.png');
  background-repeat:no-repeat;
}
.sprite-source-foo {
  background-position: 0 0;
  width: 25px;
  height: 25px;
}
.sprite-source-bar {
  background-position: 0 -25px;
  width: 35px;
  height: 15px;
}
.sprite-source-baz:hover {
  background-position: -37px -12px;
  width: 12px;
  height: 12px;
}
.sprite-source-baz {
  background-position: -25px -12px;
  width: 12px;
  height: 12px;
}

As you can imagine, this output can get quite big when working with large numbers of images. There are better ways to write those CSS declarations, especially that first line. Luckily, one of the configuration options Glue offers is the ability to specify a Jinja template to use when generating the style sheet. Jinja is a simple Python templating engine. This allowed us to reduce the size of the resulting rules dramatically, and also add comments to warn other users that the file was auto-generated:

/* This file is generated by an automatic script.
   Do not attempt to make changes to it manually! */
.sprite {
  background-image:url('/path/to/sprite.png');
  background-repeat:no-repeat;
}
.foo {
  background-position: 0 0;
  width: 25px;
  height: 25px;
}
.bar {
  background-position: 0 -25px;
  width: 35px;
  height: 15px;
}
.baz:hover {
  background-position: -37px -12px;
  width: 12px;
  height: 12px;
}
.baz {
  background-position: -25px -12px;
  width: 12px;
  height: 12px;
}

With this new sprite process in place, we can now create new sprites in just a few simple steps:

  1. Drop a new image into the /source folder.
  2. Run the Glue command to re-sprite the images together and re-generate the CSS.
  3. Add the appropriate markup to the page: <i class="sprite foo"></i>

You can, of course, debate the semantic appropriateness of abusing the <i> element in this way. The benefit of using this type of markup for sprites is that it will be familiar to anyone that has used Bootstrap icons.

Compression

While Glue originally came bundled with the OptiPNG library, it was removed in version 0.9, so it is highly recommended to run the resulting sprite through an optimisation tool before putting it live. There are many to choose from, both online and command line based, including:

Challenges

While many icons and decorative images are fairly simple to drop into a design, there are some challenges when using sprites.

Hovers on parent elements

While Glue provides a simple way to specify the :hover image for an individual icon, it can’t know when you want an icon to change in response to a parent element being hovered, e.g. changing an icon’s colour when the entire <div> is hovered. This common pattern can be addressed through clever manipulation of the Jinja template:

.sprite {
  background-image: url('/path/to/sprite.png');
  background-repeat: no-repeat;
}
{% for image in images %}
  {% if image.pseudo %}
    .sprite .sprite-container{{ image.pseudo }} .{{ image.label }},
  {% endif %}
  .{{ image.label }}{{ image.pseudo }} {
    background-position: {{ image.x ~ ('px' if image.x) }} {{ image.y ~ ('px' if image.y) }};
    width: {{ image.width }}px;
    height: {{ image.height }}px;
  }
{% endfor %}

Here we are checking for a pseudo state, and if one is present we add an extra rule that triggers the image change if a parent element with the specific class of .sprite-container is hovered as well. Now we can create markup like this:

<div class="calendar sprite-container">
  <i class="sprite calendar-icon"></i>
  <h2>Calendar</h2>
</div>

When the <div> is hovered, the hover state of the icon will be triggered. A similar trick can be used to implement a ‘selected’ state as well.

Identical images

A harder problem to solve is what to do about duplicate images. If you use the same icon to stand for multiple different things, you either have to use the same class name for all of those things, which is not very flexible when you’re working with data coming from the back end, or put several differently-named identical images in your /source folder and sprite, which is not brilliant for file size. For now we’re using duplicate images, but we continue to investigate alternatives.

Summary

If you want to make the best use of CSS sprites in a large organisation, and for performance reasons you really should, then you’re going to need to make it as easy as possible for everyone that uses them to work with the same centralised source image. Automating the task of adding and removing individual images from the company sprite removes a lot of the hassle and wasted time associated with working with sprites.

comments powered by Disqus