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.
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:00;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:00;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:
- Drop a new image into the /source folder.
- Run the Glue command to re-sprite the images together and re-generate the CSS.
- 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:
- TinyPNG.com
- Smushit
- Pngcrush
- ImageOptim
- ...and many others, including some that are available as Grunt tasks.
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;}{%forimageinimages%}{%ifimage.pseudo%}.sprite.sprite-container{{image.pseudo}}.{{image.label}},{%endif%}.{{image.label}}{{image.pseudo}}{background-position:{{image.x~('px'ifimage.x)}}{{image.y~('px'ifimage.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:
<divclass="calendar sprite-container"><iclass="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.