Stop Making Sprites (Compass, Sass, and PNG Sprite Generation)
Update 01/11/12: new code at the bottom of this post.
Sass has been kicking around for a while, but I hadn’t given it a try until just recently. Sass usually goes hand-in-hand with Rails, Compass makes it so easy to run Sass on standalone projects that I’ve started using it on nearly everything. The result is faster, DRYer, more enjoyable coding. The biggest benefit for me has been Compass’s sprite generation, which — if done right — can cut down your coding time and filesize.
Sprites are an optimization best practice, but they’re no fun to work with. Making them by hand requires a lot of nudgy mechanical math, and most auto-spriting tools create more work than they save. Compass does sprites the smart, Sassy way: by compiling them from your code and filling in the CSS rules for you.
It also does things the Rails way, in that it provides conventions to follow rather than configuration to specify. I’ll walk you through my process for Compass spriting on a simple, standalone project.
This is how I’m laying out the example project:
index.html config.rb /assets - /images - /content - /structure - common-scc61cc8905.png - transparent-s72a23cbe19.png - /common (to be sprited) - /transparent (to be sprited) - /unique - /icons - /css - all.css - print.css /compile - /pngquant - pngquant - /images (PSDs) - /sass - all.scss - print.scss - /core - /content - /pages
I like to separate any compile-to stuff from the actual final output — hence “compile” and “assets”. If a client receives the buildout and doesn’t want to use Sass/Haml/Coffeescript/whatever, they can just delete the compile folder for a clean build without any leftovers.
Sass allows you to break CSS up and @import it into a single file at compile time, and I tend to go crazy with this feature. CSS feels nicer, to me, chunked up like this and navigated with something like PeepOpen. All of my Sass files are imported into all.scss, and I watch that file alone with the command:
compass watch compile/sass/all.scss
The key to precutting images for sprite generation is to know Compass’s restrictions:
- All images in a sprite should come from the same folder.
- Sprites MUST be vertical in layout — there's no grid, just a big stack.
- Anything you want to offset from the top/left/right of your element, or align right/center, you’ll need to specify using Compass variables.
- Because of the one-directory-one-sprite convention, you should separate images that you need to use different color depths. For example, I use “transparent” for all my 32-bit transparent PNGS, and “common” for the rest).
- No JPGs.
- Compiling sprites can take some time. I’ve found it’s easiest to just use image-url() until the end of a project, then do all the sprite setup in one go.
First, you’ll need to import your sprites from within a Sass file.
@import "common/*.png"; @import "transparent/*.png";
Any preferences you need to set for specific images need to be set BEFORE these imports, so it might actually look like this:
$common-divider-position: 20px; $common-divider-repeat: repeat-x; $common-go-button-position: 100%; @import "common/*.png"; $transparent-tabcontrols-a-position: 50%; @import "transparent/*.png";
(Where do names like "transparent-tabcontrols-a-position" come from? These are magic variables generated by the sprite/folder name and the file name [transparent/tabcontrols-a.png])
The only times you can’t get around these config options is when you want to center, right-position, or repeat an image. Otherwise, I try to avoid setting them here if I can accomplish the same result by changing an image directly.
Later in your stylesheet, you’ll want to replace the usual background-image and background-position rulse with something like this:
@include transparent-sprite(tabcontrols-a, $offset-x: 50%);
This outputs the path to your sprite and the appropriate BG position. The offset is optional and rarely used, but important for centered/right-aligned backgrounds.
Generating the Sprites
At this point in the example project, Compass is outputting “common” and “transparent” PNGs and linking them up in the CSS. So far so good, but there’s one problem: Cramming so many PNGs into one sprite has probably increased the color count past the 8-bit 256-color limit. When this happens, Compass’s PNG engine (chunky_png) assumes you want 24-bit color depth and saves your sprite accordingly — which generally means larger files and issues with IE6 transparency. Ideally, we want to re-render our sprite at 8-bit depth after Compass finished saving the sprite.
The quickest way would be to open the resulting image in Photoshop and “Save For Web”, but this is pretty painful to do dozens of times per project, so I prefer to automate it.
To auto-downsample the sprite, you’ll need a program that can smartly choose and dither the color. I use improved Pngquant (OS X-only) and save it in the project file at /compile/pngquant/pngquant. Improved Pngquant reduces the color depth of high-color PNGs and, does a fantastic job of it — check out the original software (and some non-OS X versions) over at libpng.
Then, we’ll set up Compass’s post-sprite hook in the config file to:
- Create a leaner, 8-bit version of the sprite and delete the original.
- Make sure NOT to do this for images that should actually have 24-bit depth (I usually name this sprite “transparent” and just check against the filename).
- Fire off a Growl notification when compiling finishes. (Note: You’ll need the ruby-growl gem and Growl for this)
If you’re ”compass watch”ing the Sass file, the sprite should start building once you make any modifications (even a small change should do it). You’ll get a Growl notification when it’s finished (1-3 minutes in my case, depending on how many images the sprite contains).
Sometimes the sprite colors might get a little crappy — This can happen if there are too many dissimilar colors into a single sprite, and Pngquant can’t find good compromises for them all. To fix, break the images into more sprites, grouped by color palette.
That’s all I know so far — Compass is maturing quickly, so we can probably expect even more improvements to sprite-handling in the future. Check out Compass’s spriting tutorial, and let me know if you have a different sprite method you’ve enjoyed working with.
Since writing this post, I’ve adjusted my original compass.rb code quite a bit. You can see the revised version in my FED Starter Kit. Also, take a look at Tim Kelty’s, which fixes an issue with the original code and updates the compass growl call.