CSS Tilesheets -- Scaleable "Sprites"

CSS3

One of the more powerful techniques in site-building is the use of the incorrectly named "CSS Sprites"... Since they are not animated or layered or what have you they aren't truly sprites, we just call them that because they work like the tile-sheets many early (and even some modern, see Id's "megatextures") game engines used to store the different frames and images used for sprites and backgrounds.

The main advantage putting as many of your images as is practical in a single file is that means only one file request (called a "handshake") to the server. Every time you request a file from a server it introduces overhead of anywhere from a tenth of a second to a second or more depending on server load, traffic load between you and that server, and a host of other factors REGARDLESS of your connection speed!. The less separate files you request, the lower the server load, and the faster the page loads. That's why you should TRY to combine together as a single file what scripting you can, why you should TRY to combine together your CSS into a monolithic file at the very LEAST for deployment, and why things like icons or special corners/dividers can be sped up as a single file. That said one SHOULD try to keep as much JavaScript and CSS separate from your HTML as then they can be "cached" avoiding the request entirely on sub-pages or revisits. Sadly the people crapping all over websites by encoding their images as the outdated base64 and certain tools like Google PageSpeed and CSSLint are offering some REALLY faulty advice on this! Don't let those tools make you the tool...

As the technology improves, the use of images as part of your theme can start to run into a new issue... being shown scaled to a larger size. Apple's retina displays and devices like the Kindle Fire HD are introducing 1080p and even so called "4k" displays in sizes of 10 inches or less, so they enlarge the content making the text clearer and sharper; but in the process they are also making images on the pages blurry.

So how do we address this problem? A lot of people dive for SVG since it is a vector format -- the problem is the lack of support in legacy browsers, the file format being harder to work with, but worst of all it has four major failings:

  1. The floating point math chews on battery life for mobile devices
  2. Below certain sizes it loses fine details to the point you often can't make it out
  3. It's harder to use positioning to leverage a single file for multiple images.
  4. For a vector format being that it is XML it's not the most efficient data transfer method so far as file-size is concerned. Far from it in fact! The files can in fact be many times larger than they would be in a proper binary vector format like fonts, or even as a somewhat large .png or JPEG.

The next approach -- and it's a decent one -- is to store things in a font. The only problem with this is fonts are monochromatic so if you want anything fancier than a foreground colour, that's not gonna fly. While there IS a "styled font" format Microsoft has proposed and has working demos of, implementation (as of early 2016) and widespread acceptance outside of Microsoft is still a good ways off.

Still, as a font is a single file and detail rendering is kind of what fonts do, it's one of the better choices.

So what else can we do?

The final answer -- and the one we'll be working with here -- is one Apple uses, just use a higher resolution image and have the browser (or OS in Apple's case) scale it. TRADITIONALLY doing so often resulted in the image looking like garbage at the desktop pixel size and wasted bandwidth, but the technology has vastly improved since the days of Microshaft Internet Exploder 6 and Nyetscape 4. The resulting file will be bigger, but shouldn't be so bad we can't work with it.

Trying to use browser scaling of images used to have two shortcomings -- first you could only use the IMG tag since that was the only way to resize an image. Mind you doing this with IMG if the image is in fact content is a common practice now -- we USED to consider it bad practice and a waste of bandwidth and often resulted in ugly results -- but improvements in image scaling algorithms (many browsers use anisotropic filtering or similar techniques) and the presence of higher and higher resolution displays has made it something we need to give serious consideration to.

Hence why my plate images and even the site logo for this site are stored at twice the resolution needed for a stock desktop display -- with a fallback for older browsers at the native size.

That first problem of not being able to scale background images has been eliminated in modern browsers as CSS3 has added the background-size property.

The second problem is caused by the answer to the first! It's CSS3 only, so what about older browsers? CSS3 also provides the solution -- do it both the traditional way using the low/native resolution image for older browsers, and the CSS3 background-size way on modern browsers. How do we target the older browsers? WE DON'T, we set the older browser CSS as the default and use CSS3's media queries to target the modern browsers! (I'm often surprised how few developers and designers even consider that approach!) Making CSS that only targets CSS3 browsers is surprisingly easy if you think about it...

@media (min-width:1px) { /* CSS3 only stuff here */ }

Cute but simple trick that will nab only CSS3 capable browsers... that's completely valid and not a "hack" in any sense of the word. Gotta love it!

So let's say you had four fancy styled icons that also have hover states. The "easy" way that's a 4x2 grid. As we are using browser scaling I HIGHLY recommend you put two pixels of transparency between each item. For the time being there's no real reason to make the "retina" or high resolution version be larger than twice the desktop size, so let's say that on old school low resolution desktop they would be 64x64 each, we would design them as 128x128, resulting in a 512x256 image. Since we need a fallback, we'd have a second image at 256x128 for 'legacy' use. That means something like these:

Modern "Scaleable Tilesheet", 17.3k

Babylon 5 Alien Race Logos 512x256

Legacy "CSS Spritesheet", 6.94k

Babylon 5 Alien Race Logos

I kept them fairly simple here, if you were actually working with monochromatic images like these, I'd say go the webfont route. The top row will be our normal state, the bottom row when we have the mouse over them.

Real World Usage Scenario

Let's say those icons will all be part of anchors in a list, pointing at subsections of the site. It would be an unordered list, anchors inside it, and to make life simple we'll tack two italic tags inside them as our hook for showing the appropriate part of our tile sheets. The reason I suggest nesting two tags will become apparent when we apply our style. Each tile would need its own class so we can target each part of our parent image. I suggest putting this on the LI so that if need be you can target any of the child elements for style (like say...colour), not just the icon. Done properly we shouldn't need to bloat out the markup with too many classes or ID's since we can leverage semantics for most of our grunt-work.

So something like this would be the appropriate markup.

<ul class="tileDemo">
	<li class="league">
		<a href="#">
			<i><i></i></i> League of Non-Aligned Worlds
		</a>
	</li>
	<li class="centauri">
		<a href="#">
			<i><i></i></i> Centauri Republic
		</a>
	</li>
	<li class="minbari">
		<a href="#">
			<i><i></i></i> Minbari Federation
		</a>
	</li>
	<li class="narn">
		<a href="#">
			<i></i> Narn Regime
		</a>
	</li>
</ul>

Styling Step 1, Legacy FIRST!

You will often hear people talk about "mobile first" when talking web design. The idea being that mobile is the "lowest common denominator" and the "least capable of devices". IMHO this is 100% grade A farm fresh prairie pies. Any mobile device we care about at this point has CSS3 and can be targeted with media queries. They are just as capable as modern desktop browsers and as such are NOT the "lowest common denominator". The entire notion of "mobile first" to me makes little if any sense.

Whenever you are thinking accessibility, progressive enhancement or creating a plan for lowest common denominator, you have to ask "What can we target?" and "What can't we target?" Whatever you can't target is the one you should write the "blanket application" code for first!

What fits the bill for that? Simple, IE8/earlier! No CSS3, no responsive layouts, no background-size; that's our baseline. We can then later use CSS3 to enhance it ...and that means for our build process, we should be looking at the legacy implementation FIRST.

Said legacy style being the same process as normal CSS sprites, but with two little twists. First we position in percents instead of pixels so that we can share at least some of our CSS with the modern version. Second is that extra nested italic tag. Instead of using background-position we can set the outer <i>' to our display size (64x64) and overflow:hidden, to then make the inner tag our full image size using position:absolute and left/top to show our different bits of the image. Being able to state left and top separately means we don't have to say "background-size" once for every possible combination of image.

So our legacy CSS would go something like this:

.tileDemo {
	list-style:none;
}

.tileDemo li {
	display:inline;
	vertical-align:top;
}

.tileDemo a {
	display:inline-block;
	vertical-align:top;
	max-width:6em;
	padding:0.5em;
	text-decoration:none;
	color:#666;
}

.tileDemo .league a:active,
.tileDemo .league a:focus,
.tileDemo .league a:hover {
	color:#F50;
}

.tileDemo .centauri a:active,
.tileDemo .centauri a:focus,
.tileDemo .centauri a:hover {
	color:#408;
}

.tileDemo .minbari a:active,
.tileDemo .minbari a:focus,
.tileDemo .minbari a:hover {
	color:#08C;
}

.tileDemo .narn a:active,
.tileDemo .narn a:focus,
.tileDemo .narn a:hover {
	color:#E21;
}

.tileDemo i {
	display:block;
	overflow:hidden;
	position:relative;
	width:64px;
	height:64px;
	margin:0 auto 1em;
}

.tileDemo i i {
	position:absolute;
	top:0;
	left:0;
	margin:0;
	width:256px;
	height:128px;
	background:url(images/b5AliensLegacy.png) 0 0 no-repeat;
}

.tileDemo a:active i i,
.tileDemo a:focus i i,
.tileDemo a:hover i i {
	top:-100%;
}

.tileDemo .centauri i i {
	left:-100%;
}

.tileDemo .minbari i i {
	left:-200%;
}

.tileDemo .narn i i {
	left:-300%;
}

Which you can find a live demo of here:
demos/tilesheets/legacyOnly

We turn off bullets for the list, set the LI to inline since legacy browsers can't inline-block block-level tags. For inline-block behavior we target the anchor instead, setting both the list item and anchor to vertical-align:top so they all line up. (Some browsers will ignore it and try for middle if you only set the anchor or the list item.). We style the text of the anchor as we normally would with colours and hover states, targeting active, focus and hover so we nab not just mouse, but keyboard navigation as well. Nothing too out of the ordinary until we get to those italic tags.

Since they're both the same tag we just leverage child selectors for the values unique to each. Outer one a fixed size with overflow:hidden and position:relative, so we can absolute position the inner italic tag based on the parent's size and location. Remember that positioning is based on the parent hence our left and top positions being increments of 100%. This can be REALLY nice since it means we don't have to deal with irrational numbers should our tilesheet be a multiple of a prime.

Styling Step 2, Modern Browsers with CSS3

For our dynamic scalable layout we'll switch the icon size to EM, so it also scales with the font. This not only makes it friendly for retina type devices, it also makes it nicer for users who do not have that magical default 16px that so many developers have deluded themselves into believing everyone has. As a large font/120dpi/8514/125%/20px/whateverTheyCallItThisYear user, designers failing to use EM's on fonts and other scalable elements is something of a pet peeve!

Doing this is pretty simple, we use that 1px media query trick to wrap changing the width and height of the outer italic tag into 4em -- which at 96dpi with body still at 100% is 64px, for large font users like myself would be 80px, and on retina and other high resolutions display currently in circulation ranges anywhere from 80px to 128px. The inner tag likewise gets set to 16em x 8em, has our different background image assigned to it, and gets background-size set to 100%.

Which looks something like this:

@media (min-width:1px) { /* all CSS 3 capable browsers */
	.tileDemo i {
		height:4em;
		width:4em;
	}
	.tileDemo i i {
		width:16em;
		height:8em;
		background-image:url(../images/b5Aliens.png);
		background-size:100%;
	}
}

Which you can find a live demo of here which works both legacy and modern:
demos/tilesheets/modern

Taking it further, how about a transition?

The simple animation of a transition fade from one colour to another is very easy to do in CSS3 and just looks more professional. Whilst this is easy to do with our text, what about this image? Can we do it? Yes, yes we can. How? Opacity.

What if we make it so our non-hovered state was the default appearance of the outer <i>, and the hovered state was the that of our inner one? We could then set the outer one to opacity:0; normal, and opacity:1 when hovered with a transition on it.

That goes a little something like this:

@media (min-width:1px) { /* all CSS 3 capable browsers */
	.tileDemo a {
		transition:color 0.5s;
	}
	.tileDemo i {
		height:4em;
		width:4em;
		background:url(../images/b5Aliens.png) 0 0 no-repeat;
		background-size:16em 8em;
	}
	.tileDemo .centauri a>i {
		background-position:-4em 0;
	}

	.tileDemo .minbari a>i {
		background-position:-8em 0;
	}

	.tileDemo .narn a>i {
		background-position:-12em 0;
	}
	.tileDemo i i {
		width:16em;
		height:8em;
		top:-4em;
		background:url(../images/b5Aliens.png);
		background-size:100%;
		opacity:0;
		transition:opacity 0.5s;
	}
	.tileDemo a:active i i,
	.tileDemo a:focus i i,
	.tileDemo a:hover i i {
		opacity:1;
	}
}

We tell the browser we want a half second fade on the text, modify the outer I to show the appropriate slice of the background. I use the direct child selector so we only apply background-position to the outer LI. Normally I avoid this selector as old browsers don't support it, but we're in a media query, it's supported!!!

From there we just adjust the outer one so that it's always offset -4em in position to always "show the hover state" and set the opacity to 0 so we can't see it. We have to do that position change otherwise when we mouse-out the image will flip instantly failing to show our transition. Add the transition, and the hover state for changing the opacity from transparent to opaque, and we're in business.

The live demo of that can be found here:
demos/tilesheet/transitions

As with any of my demos the entire directory...
demos/tileSheet
...is unlocked for easy access to the gooey bits and pieces.

Conclusions

Using scaled up images and sizing them dynamically used to be something I railed against as a waste of bandwidth; but bandwidth is less and less of a concern compared to things like handshakes and the presence of increasingly higher resolution displays packing more "pixels per inch"? That means that our interface design methods have to start addressing that! As nice as vector formats are, they have sortcomings at lower resolutions and formats like SVG can get really bloated really quick, even compared to an oversized bitmap! There's a reason the two groups that championed SVG's creation -- Adobe and Microsoft -- dropped it like a hot potato a decade ago! Then for some bizarre reason the W3C and WhatWG picked up that ball dusted it off, and ran with it...

Having more alternatives on how to do it just means another tool in the toolbox. Is it something I'd use all the time? No. Are there cases where it's the "best tool for the job"? Yes.

Uncle Red So remember, if women don't
find you handsome, they should
at least find you handy!

Far too often it seems developers learn just one way of doing things then use it to the exclusion of all others. It's like the person who only knows how to use a hammer -- suddenly all problems look like nails to them. While you can use a hammer (Or as Uncle Red calls it, a flathead screwdriver) to drive a screw, the result usually leaves much to be desired.

So, hope this helps some folks. I'm using a mix of various techniques on this site partly from the "right tool for the right job" mentality, but also so I can keep learning. Something I'm always trying to drive home is the day you stop learning, is the day the world leaves you behind. Same as how the day you stop questioning things, is the day the scam artist own your hide.

Projects

  • elementals.js
    A lightweight JavaScript library focusing on cross browser support, ECMAScript polyfills, and DOM manipulation.
  • eFlipper.js
    An image carousel script using elementals.js
  • eProgress.js
    A JavaScript controllable progress bar using elementals.js. Based on the nProgress project that relies on the much heavier jQuery library.

/for_others

Browse code samples of people I've helped on various forums. These code snippets, images, and full rewrites of websites date back a decade or more, and are organized by the forum username of who I was helping. You'll find all sorts of oddball bits and pieces in here. You find any of it useful, go ahead, pick up the ball, and run with it.