The Ecstasy and Agony of Compass Sprite Generation, Part 2

css

In the first part of our tutorial on Compass sprites, I showed you how awesome it was that we could make Compass do all the hard work for us by simply throwing our icons into their own folder and telling Compass what we wanted to do. If it were only that easy all the time.

The Agony

Alas, generating sprites with Compass does have its pitfalls. Consider the growing use case of responsive web design and HiDPI (a.k.a. Retina®) images. To be truly bandwidth friendly, you only want to serve the HiDPI images to devices that will take advantage of them. That means two sets of icons and using media queries.

Problems Matching Icon Sprites

One would think, logically, that icons that have matching names would be arranged the same way when running the same command on two different folders. The reality is, not so much. To illustrate this, we’re going to start with a fresh set of icon sprites. Let’s get started by adding some social media to our web page.

The Icons

For my example, I’m going to use the Incontexto Inside icon set. We’ll use the 32×32 pixel flavor for our regular resolution displays, and then we’ll need the 64×64 pixel size when we go HiDPI. But as always, we’ll start with sane, semantic HTML:

<footer>
	<ul>
             <li><a href="http://twitter.com/taupecat" title="Twitter" class="twitter">Twitter

             <li><a href="http://facebook.com/taupecat" title="Facebook" class="facebook">Facebook

             <li><a href="http://www.linkedin.com/in/taupecat" title="LinkedIn" class="linkedin">LinkedIn

             <li><a href="https://plus.google.com/116899694717638684174/posts" title="Google+" class="googleplus">Google+

             <li><a href="http://pinterest.com/taupecat/" title="Pinterest" class="pinterest">Pinterest

       </ul> 
</footer>

And now, a little SCSS to get us the same horizontal style we had going in part 1:

footer {
	li {
		float: left;

		a {
			margin-right: 15px;
		}
	}
}

For this example, we’re going to go with image replacement to get our social media names out and our social media icons in. Personally, I like to use a slight variation of the Kellum Method. But instead of inserting an ugly, non-semantic “ir” class into our markup, we’re going to use, once again, the ultra-slick Sass @extend function with a placeholder selector. (I’m not going to go much into the details of placeholder selectors here when there is plenty of other information about them elsewhere on the web. Suffice to say, they rock.)

%ir {
	display: block;
	overflow: hidden;
	text-indent: 102%;
	white-space: nowrap;
}

Adding that to the SCSS we wrote out above, our preliminary style comes out to:

footer {
	li {
		float: left;

		a {
			@extend %ir;
			margin-right: 15px;
		}
	}
}

Now, we just need to create our sprites to fill in the blank areas. Taking the five icons of importance to us (32×32 pixel version, please) and putting them inside a folder called “social-icons”, we end up with a directory structure like so:

/(Document Root)
	images/
		social-icons/
			facebook.png
			googleplus.png
			linkedin.png
			pinterest.png
			twitter.png
	index.html

Now, at the top of our SCSS code, we’ll put in the code to generate our sprites, but this time, we’re going to use the sprite-map function.

$social-icons: sprite-map("social-icons/*.png");

This is going to do essentially the same thing our @import directive in part 1, but it’s going to give us a slightly different set of tools to use in the rest of our Sass code.

a {
	@extend %ir;
	margin-right: 15px;
	background: $social-icons no-repeat;

	&.twitter {
		background-position: sprite-position($social-icons, twitter);
	}

	&.facebook {
		background-position: sprite-position($social-icons, facebook);
	}

	&.linkedin {
		background-position: sprite-position($social-icons, linkedin);
	}

	&.googleplus {
		background-position: sprite-position($social-icons, googleplus);
	}

	&.pinterest {
		background-position: sprite-position($social-icons, pinterest);
	}
}

We’re well on our way, with our social media bar looking something like this:

Oops! Our social media icons are looking a little chopped off!

Except that I’m sure we’re going to want to see all of the icons, not just the top halves. Fortunately, Sass has some built-in functions to help us with this.

Determining Icon Dimensions

When we generated our sprite map, we also generated some Sass functions that can give us the width and height of each of our icons any time we need them. Since this is a pretty basic example, and all of our icons have the same dimensions, we can apply them to the <a> tag globally and not to each social media link separately. We’ll just user the Twitter icon’s dimensions as our basis and apply the dimensions like so:

a {
	@extend %ir;
	margin-right: 15px;
	background: $social-icons no-repeat;
	@include sprite-dimensions($social-icons, twitter);

	[...]
}

A quick reload and voilà!, our sprites are now fully visible:

Yay! Our social media sprites in all their glory!

Enter HiDPI

HiDPI, often referred to as Retina display, is showing up on more and more devices every day. First shown off by Steve Jobs when premiering the iPhone 4, high-resolution displays are now on iPads, Apple laptops, and increasingly on non-Apple computers and devices. There’s no getting around it, the future of displays is high-density.

As beautiful as these displays are, viewing images not optimized for them is a less than satisfying experience. We want to be forward thinking, so we’re going to use CSS3 media queries to target HiDPI displays with larger, sharper images. Or, at least that’s the plan.

It should be as simple as taking 64×64 pixel social media icons, generating a sprite map with them, and substituting them for the lower resolution 32×32 pixel icons we’re serving up to non-HiDPI displays. Let’s give it a try.

In our “images” directory, we’ll create a new folder called “social-icons-hidpi” and put in the same five social media icons we did in the non-HiDPI directory, but this time in the 64×64 pixel variety. Then putting the requisite sprite generation code in the top of our SCSS file:

$social-icons-hidip: sprite-map("social-icons-hidpi/*.png");

We now have a second sprite file, twice the file dimensions as the first, which is what we want. But here we run into our first problem. The icons are in different orders! Instead of placing the images in the sprite file in alphabetical order, so that folders with icons that have the same names are generated in the same way, Compass arranges the icons from largest file size at the top to the smallest on the bottom. And since smaller (in terms of dimensions) versions of the same icons don’t necessarily correspond to their larger counterparts, we can easily end up with different orders in our finished sprites.

Hmm, these icon sprites don’t quite match. That may come back to bite us later.

If we were to take this path to its logical conclusion, we would set the background image for our HiDPI media query to the new HiDPI sprite, and adjust the background size to the size of the non-HiDPI sprite. The resulting code would be:

@media only all and (-webkit-min-device-pixel-ratio: 1.5) {
	background-image: sprite-url($social-icons-hidpi);
	background-size: 32px auto;
}

(I’m using the webkit-only HiDPI media query for this example to keep the code simple. In practice, you’d want to use a more complete, browser-agnostic media query or better yet, use a Sass mixin.)

What the what? That’s not Twitter!

So here’s the result, a beautiful, HiDPI-optimized display of social media icons.
Only problem is, they’re in the wrong order.

You’d think that the way around this problem is restating the background-position inside the media query, this time with the HiDPI sprite:

@media only all and (-webkit-min-device-pixel-ratio: 1.5) {
	background-image: sprite-url($social-icons-hidpi);
	background-size: 32px auto;

	&.twitter {
		background-position: sprite-position($social-icons-hidpi, twitter);
	}

	&.facebook {
		background-position: sprite-position($social-icons-hidpi, facebook);
	}

	&.linkedin {
		background-position: sprite-position($social-icons-hidpi, linkedin);
	}

	&.googleplus {
		background-position: sprite-position($social-icons-hidpi, googleplus);
	}

	&.pinterest {
		background-position: sprite-position($social-icons-hidpi, pinterest);
	}
}

Dangit! This ain’t right either.

But now we have a mismatch of background-position versus background-size, and aside from the sprite right at the top of the file, nothing is in its right place. Some icons are downright missing. So we need to compensate for this:

@media only all and (-webkit-min-device-pixel-ratio: 1.5) {
	background-image: sprite-url($social-icons-hidpi);
	background-size: 32px auto;

	&.twitter {
		background-position: 0 round(nth(sprite-position($social-icons-hidpi, twitter), 2) / 2);
	}

	&.facebook {
		background-position: 0 round(nth(sprite-position($social-icons-hidpi, facebook), 2) / 2);
	}

	&.linkedin {
		background-position: 0 round(nth(sprite-position($social-icons-hidpi, linkedin), 2) / 2);
	}

	&.googleplus {
		background-position: 0 round(nth(sprite-position($social-icons-hidpi, googleplus), 2) / 2);
	}

	&.pinterest {
		background-position: 0 round(nth(sprite-position($social-icons-hidpi, pinterest), 2) / 2);
	}
}

Now we’re taking the second value of the background-position property of our HiDPI sprite, dividing that by 2 to get our size-adjusted position, and setting our Y background-position value to the result. OMG the hoops we have to jump through to get here! I highly recommend you write a Sass functionto do this for you.

Finally! We’ve got the right sprites, in the right order, in glorious high resolution.

Figuring Out background-size

As an aside, there’s no “automagic” way to determine what our background-size value should be. The X value (the first one), should always be the width of the lower-resolution sprite file that we’re replacing with the higher-resolution one; the Y value (the second one) can then just be set to auto. But neither Sass nor Compass have any automatic functions or mixins that will tell you this. The sprite-dimensions mixin only gives you the dimensions of a named sprite within the file, not the file itself. You will need to be aware of the value and when it changes.

In Conclusion

This is just a scratching of the surface when it comes to the challenges involved in using Sass and Compass sprite generation. In general, it’s a terrific tool that could conceivably save you hours of tedious, monotonous work. But potentially, it can make you want to pull your hair out as you try to find work-arounds for every special case you may come across. Hopefully the maintainers of Sass and Compass will improve the sprite generation process in future releases. In the meantime, there are other developers seeking a variety of solutions to these issues (see Retina Sprites for Compass and Using Compass to generate normal and retina sprites (from which some of my code above was derived), just to list two.  This doesn’t negate the usefulness of Compass sprite generation, but it does make things challenging at times.  Best of luck in your Sass/Compass project!

Tracy Rotton

Tracy Rotton is a web developer for RP3 Agency in Bethesda, Maryland with a passion for standards-based front-end development. A big fan of WordPress, she is a contributor to the new WordPress default theme, Twenty Thirteen. When Tracy isn't building websites, she enjoys skiing, watching Redskins football and spending time with her two young children.

Comments

2 comments on “The Ecstasy and Agony of Compass Sprite Generation, Part 2