Activity Indicator with CSS3

Works on desktop Safari, desktop Google Chrome, Android, iPhone, iPod Touch and iPad.

You’ve seen them. The things on the screen that mean you’ve got to wait for some activity to complete. They’re called activity indicators. They’re usually several pieces arranged in a circle. They spin around. And if things go really bad, you could be watching them spin for quite some time. Hopefully not.
activity indicator

Usually people make these with images. There are even Web sites dedicated to making this spinning baubles for you in animated gif format. Except that gif stink. Pngs look better. I’m going to show you two ways to implement an activity indicator. One starts off with an image that has been converted into 64 bit data. We’re talking about dataurls here. Dataurls eliminate the need for separate images. It’s the same image but reduced to data which you can paste directly in your document. Heck, you can paste it directly into your CSS file so you never have to worry about losing an image or rewriting the image path when you move the CSS. Cool, huh? And if you start with a large image, you can scale it down for small screens and low resolution devices, and scale it up for hi res devices, like the iPhone and iPod Touch retina display. Cooler still, you can make the activity indicator completely out of HTML and CSS, no images, no dataurl. And by using a scale transform, you never have to worry about resolution. It will always render smoothly.

So, let’s look at dataurls. The concept is simple. Instead of having a resource reside as an external file, you convert it into a 64 bit data sample which you can include in your document. In the case of an image, it’s basically embedding it in your HTML/CSS. You can encode images as dataurls in many ways, using PHP, Python, etc., or you can simply upload a file to a Web site that will conveniently do it for you. A dataurl looks something like this (Note: I trimmed off the data to fit here):

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATEAAAEwCA+

For an image, you would do something like this:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATEAAAEwCA+...">

When you create your first dataurl you’ll probably be shocked at how big the resulting code is. Don’t worry. Somehow this winds up being more efficient for download and rendering than a traditional image. Besides being the source for an inline image, you could also embed the dataurl into your CSS as a background image. As I mentioned, my original image was large to allow for scaling up or down without loss of quality. I therefore add in some resizing to the CSS for the size of the element and the size of the background image:

.activityIndicator {
	/*
	The original height is:
	height: 304px;
	width: 305px;
	*/
	height: 40px;
	width: 40px;
	-webkit-background-size: 40px 40px;
	margin: 0px auto;
	background-image: url("data:image/png;base64,iVBORw0KGgoAAAANUb...");

This is what I used to produce the image above. Now we need to animate it. That’s actually quite easy, just a couple of lines of CSS:

.activityIndicator {
	/*
	The original height is:
	height: 304px;
	width: 305px;
	*/
	height: 40px;
	width: 40px;
	-webkit-background-size: 40px 40px;
	margin: 0px auto;
	background-image: url("data:image/png;base64,iVBORw0KGgoAAAANUb...");
	-webkit-animation-duration: 1s;
	-webkit-animation-iteration-count: infinite;
	-webkit-animation-timing-function: linear;
	-webkit-animation-name: spinnerAnim;
}
@-webkit-keyframes spinnerAnim {
	0% { -webkit-transform: rotate(0deg); }
	100% { -webkit-transform: rotate(360deg); }
}

By using the keyframe animation, we can implement the continuously spinning activity indicator without using JavaScript. But, as I mentioned, dataurls tend to be verbose and are rather unsightly. I therefore came up with a reproduction of the activity indicator using only HTML and CSS3. At the end of this post are links to the working file. Download or view source of the online document to see what the dataurl looks like. It’s not pretty, but it is efficient. However, by reproducing the same image using markup and CSS, we can achieve the same result with even less code. The markup isn’t terribly complex, two parent contains and twelve blades:

<div id="activityIndicator">
	<div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
		<div class="blade"></div>
	</div>
</div>

That’s really all we need, the rest is done with CSS3 properties. You’ll notice that all the blades have the same class “blade.” We don’t need to give each one anything special because we can use CSS3 selectors to indicate each blade individually. To do this we’ll use the :nth-child selector. We first give all the blades some generic styling. Then we start from the second blade using the CSS3 nth-child selector: .blade:nth-child(2), then .blade:nth-child(3), etc. Since there are twelve blades, we need to rotate each one 30 degrees more than the previous. We also need to change the color by approximately 21.25% to go from and rgb value of 0 to 255. Since rgb values expect positive integers between 0 and 255, we need to round the float off to the nearest whole number. As you can see, CSS3 selectors allow us to create complex structures with minimal markup and CSS.

#activityIndicator {
	position: relative;
	width: 130px;
	height: 130px;
	margin: 0 auto;
	-webkit-perspective: 500;
	-webkit-animation-duration: 1s;
	-webkit-animation-iteration-count: infinite;
	-webkit-animation-timing-function: linear;
	-webkit-animation-name: spinnerAnim2;
}
@-webkit-keyframes spinnerAnim2 {
	0% { -webkit-transform: rotate(0deg) scale(.29); }
	100% { -webkit-transform: rotate(360deg) scale(.29); }
}
#activityIndicator > div:first-of-type {
	margin-left: 58px;
	margin-top 0;
	width: 50%;
	height: 50%;
}
#activityIndicator .blade {
	position: absolute;
	height: 40px;
	width: 14px;
	background-color: rgba(234,234,234, .30);
	-webkit-border-radius: 10px;
	-webkit-transform-origin-x: 50%;
	-webkit-transform-origin-y: 165%;
}

#activityIndicator .blade:nth-child(2) {
	-webkit-transform: rotate(30deg);
	background-color: rgba(212,212,212, .70);
}
#activityIndicator .blade:nth-child(3) {
	-webkit-transform: rotate(60deg);
	background-color: rgba(191,191,191, .70);
}
#activityIndicator .blade:nth-child(4) {
	-webkit-transform: rotate(90deg);
	background-color: rgba(170,170,170, .70);
}
#activityIndicator .blade:nth-child(5) {
	-webkit-transform: rotate(120deg);
	background-color: rgba(149,149,149, .70);
}
#activityIndicator .blade:nth-child(6) {
	-webkit-transform: rotate(150deg);
	background-color: rgba(128,128,128, .70);
}
#activityIndicator .blade:nth-child(7) {
	-webkit-transform: rotate(180deg);
	background-color: rgba(106,106,106, .70);
}
#activityIndicator .blade:nth-child(8) {
	-webkit-transform: rotate(210deg);
	background-color: rgba(85,85,85, .70);
}
#activityIndicator .blade:nth-child(9) {
	-webkit-transform: rotate(240deg);
	background-color: rgba(64,64,64, .70);
}
#activityIndicator .blade:nth-child(10) {
	-webkit-transform: rotate(270deg);
	background-color: rgba(42,42,42, .70);
}
#activityIndicator .blade:nth-child(11) {
	-webkit-transform: rotate(300deg);
	background-color: rgba(21,21,21, .70);
}
#activityIndicator .blade:nth-child(12) {
	-webkit-transform: rotate(330deg);
	background-color: rgba(0,0,0, .70);
}

You can try this out online or download the source code.

Update: September 14th, 2010
I forgot to mention one thing. If you look at the styles on #activityIndicator .blade you’ll notice the last two property definitions:

	-webkit-transform-origin-x: 50%;
	-webkit-transform-origin-y: 165%;

By setting the transform origin x value to 50% we fix the horizontal rotation to the blade’s center. By setting the transform origin vertical value to 165% we define the turning point at that distance from the start of the blade. Together these values cause the blades to rotate around leaving and empty circular space in the center, thus reproducing the appearance of the png image.