It’s hard to stifle a smile any time I get a glimpse of the future thanks to WebKit-based browsers like Safari and Chrome (and their mobile counterparts on iOS and Android devices). That happened today when I discovered a way to make an iPhone-style spinner without any images, just CSS.
First attempt
I have been working on a project that targets mobile WebKit browsers so it was natural to explore using -webkit-animation
to handle the image rotation. My initial idea was to use a single PNG image as a mask over a colored background which makes it possible to change the color of the spinner in code. Here’s how that looked in CSS:
p#spinner {
height: 62px;
width: 62px;
overflow: hidden;
background: #000;
-webkit-mask-image: url("data:image/png[...]");
-webkit-mask-size: 62px 62px;
-webkit-animation-name: rotate;
-webkit-animation-duration: 1.5s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
}
This is how it looks in a browser: Image mask rotation demo
The examples in this post all target WebKit browsers so I’d suggest you fire up Chrome or Safari for viewing the demos. Better yet, try a mobile WebKit browser.
It felt like a win to use a single-frame PNG image instead of an animated GIF but it didn’t look right. Webkit animations smoothly tween rotations so I ended up with what looked like a rotating image instead of the sharp ticking I was going for. Still, masking is an impressive technique that I’m sure will come in handy in another project.
CSS-only, no images
After several failed attempts to make the animation step-based, I still couldn’t get the effect right. I decided to look again into how I could do it without the image. A quick search led to a post by Kilian Valkhof who had a good idea for the basic technique. The first step is to create the bars of the animation by sizing and rotating them around an axis.
<style>
div.spinner div {
width: 12%;
height: 26%;
background: #000;
position: absolute;
left: 44.5%;
top: 37%;
opacity: 0;
-webkit-border-radius: 50px;
-webkit-box-shadow: 0 0 3px rgba(0,0,0,0.2);
}
div.spinner div.bar1 {
-webkit-transform:rotate(0deg) translate(0, -142%);
-webkit-animation-delay: 0s;
}
div.spinner div.bar2 {
-webkit-transform:rotate(30deg) translate(0, -142%);
-webkit-animation-delay: -0.9167s;
}
[...]
div.spinner div.bar12 {
-webkit-transform:rotate(330deg) translate(0, -142%);
-webkit-animation-delay: -0.0833s;
}
</style>
<div class="spinner">
<div class="bar1"></div>
<div class="bar2"></div>
[...]
<div class="bar12"></div>
</div>
Each of the twelve bars is rotated 30° from the one previous and given an animation delay equal to 1/12 of a second. This ensures that the bars highlight, then fade in order. All that is left is to define and call the animation for each bar to fade.
@-webkit-keyframes fade {
from {opacity: 1;}
to {opacity: 0.25;}
}
div.spinner div {
[...]
-webkit-animation: fade 1s linear infinite;
}
Here’s the final animation: CSS spinner demo.
Make it scale
If you look closely at the code you’ll notice some odd percentage values. Using percentages to build the spinner means you can make it any size just by changing the width
and height
of div.spinner
. It’s fully scalable. You can also append additional styles to change colors and add effects. Here are a few examples: Styled spinners demo.
And for those without a webkit browser handy, here’s what it looks like in Mobile Safari:
Chad Jaggers
on 21 Sep 10Thanks for this. You make it look so simple, and this is very lightweight. The Internets thank you.
Andrés Mejía
on 21 Sep 10Awesome! Thanks for sharing. Too bad it doesn’t work on Firefox.
Brad Parnell
on 21 Sep 10Exiting what can be done with WebKit based browsers and CSS. Would love to see more of what you come up with Jason!
Will most browser eventually incorporate WebKit, or will they just have to load a separate file or stylesheet to display an alternative graphic?
marcel
on 21 Sep 10this is so cool! thanks for sharing!
pixelmixture
on 21 Sep 10will never be as fast as an animated gif … I think everything that eats CPU is bad (for the moment).
why gmail its great … because it’s damn fast . why ? because it does not use these things.
mobileme.com on the other hand is slow as hell …
colin8ch
on 21 Sep 10Very elegant, and it looks great, thanks for sharing!
Andrew Johnson
on 21 Sep 10Great post Jason. Very quick and useful tutorial.
JZ
on 21 Sep 10@Andrés, @Brad – We’re only worried about mobile WebKit browsers for this project. So there are no plans to investigate alternatives for unsupported browsers. I’m confident if we had to, it would be easy to substitute an image for non-WebKit browsers.
nIC
on 21 Sep 10Are we taking CSS too far?
Joshua Pinter
on 21 Sep 10Slick. It’s been a while since you’ve made a post with code. Love it.
Thijs van der Vossen
on 21 Sep 10On the iPad they’re flashing instead of rotating. Will probably work there too once 4.2 is out.
Matt Sanders
on 21 Sep 10@nIC In most cases, yes. I think as long as no added markup is needed and that there is a clear benefit in http requests and load times then lets do it. But if the style is hideous and the support isn’t there for your audience then of course you shouldn’t go the extreme route.
Good work Jason.
Ethan Sisson
on 21 Sep 10@pixelmixture:
Who says animated gifs are the benchmark for speed? It doesn’t matter if it is as fast as an animated gif. The more important question is does the complexity of this implementation warrant any loss in performance?
This also gets us into Matt Ward’s article (linked by nIC), where he compares crazy CSS effects like this to using tables for layout. He makes his comparison based on three factors – bloat, flexibility, purpose. I agree with his general argument, but I don’t want to be too quick to pass something off as being an inappropriate use of CSS.
In this case, I think the degree of utility provided by this throbber implementation considerably outweighs its bloat. The amount of code is actually pretty conservative, when you consider that is not simply an icon that gets placed somewhere on the page, but is rather an integral UI element that could even be programmed to interact with the application more deeply than a simple image could (e.g. varying the speed, size, or color dynamically to communicate different states to the user). But more importantly it can be used and reused throughout the application and in other applications in a more-or-less drag-and-drop, per se, fashion. Which leads to the next point, flexibility.
The flexibility of this throbber kills what could be done with an image. The simplicity of the markup and styling makes it crazy-easy, as demonstrated in Jason’s article, to adapt this to almost any visual style with simple CSS tweaks. Want a different color? 2 seconds. Need it 5px wider? 2 seconds. Add a box-shadow? 2 seconds. No Photoshop required.
Purpose is where the lines get blurry. Are HTML and CSS meant to build UI elements like this? Not if you’re thinking in the realm of a web document. What about for a web application? Maybe. If this were Gecko 2 inline SVG could be used in lieu of HTML, which is certainly purposed for this type of use. Then you’d be using CSS to define rules for how an SVG image is presented, which I would feel good about personally.
Are we taking CSS too far? Yes, in using it to create complex icons when Photoshop would be better suited for that. Not when we’re making a programmable, styleable spinning throbber.
As for performance…it’s all a balance. Speed is an important consideration, but we shouldn’t unconditionally limit ourselves to only the fastest solution. There would be no inovation. Gmail was not as fast 4 years ago as it is today because browser vendors realized the need for drastically improved JavaScript performance. Lets push the limits a little bit.
Also: hardware acceleration.
fredo
on 21 Sep 10Doesn’t work on Android which is webkit based. I guess some browsers are more webkit than others. Soo – now we need multiple versions of our preloader…
DL
on 21 Sep 10That is some really cool shit right there. @JZ you make me proud to be an Oklahoman.
JZ
on 21 Sep 10@fredo – which browser are you using? It’s working beautifully for me in browser included with Android 2.2 on the Nexus One.
Alejandro Moreno
on 21 Sep 10I understand you’re targeting webkit only, but it looks to me like it would be almost trivial to target other browsers, if you know your moz and o- and -ms properties as well as you know your webkit ones:
http://css3please.com/
mwld
on 21 Sep 10you Sir, are brilliant!
Richard
on 21 Sep 10Neat, but it doesn’t look right. It looks more like it’s a ball with part of it discolored and the whole ball is rotating.
Donny
on 21 Sep 10Repeat after me : “Just because you can, doesn’t mean you should” :)
@Ethan Sisson: You make wonderful arguments against why this is not really a good idea, but I guess you’re too impressed by JZ’s technical feat :).
This IS taking CSS too far !
Anton
on 21 Sep 10nice one!!!
Ricardo
on 21 Sep 10You can do some amazingly ugly stuff with much simpler markup: http://jsbin.com/osepa4/2
Adam
on 22 Sep 10The HTML5 canvas tag is used to render the progress spinner without the overhead of downloading animated GIFs to the device.
Damien van Holten
on 22 Sep 10Nice concept by Kilian. Looks sweet. Now we can all sit back and wait while the other browser play catch up ;)
Espen Antonsen
on 22 Sep 10Same thing in canvas (as in no ugly html)
Dave E.
on 22 Sep 10This is clever and impressive technical feat, but it’s the wrong solution for the problem (i.e. “How do I add a spinner to my webpage?”)
The extraneous non-semantic mark-up plays havoc with accessibility and can interfere with layout.
A more practical solution would be to go ahead and use an animated png or gif image, or to use a js and canvas, or to embed svg.
SS
on 22 Sep 10Dave, I’d like to see some evidence that a dozen empty divs “plays havoc with accessibility.” What screen reader can’t handle that?
The constraint for this specific application is that we want to show the loading indicator as soon as possible. An image won’t fly because we’d need to make a second request to download it. And even if we were to inline it with a data: URI it’d end up much larger than the markup and styles we have now.
DL
on 22 Sep 10Sometimes, cooler is better. Sometimes, best practices are a fucking joke. Sometimes, you just need to do what you need to do and not worry about wasting a shit ton of time finding the “best” most “optimal” or “accessible” solution. Sometimes, peoples opinions are narrow or near sighted, or even worse too far sighted.
Reda
on 22 Sep 10Nice one ! but i still prefer the mask approach (or SVG ?) 12 more divs in the dom tree seems “unsemantic” to me.
vanls
on 22 Sep 10The spinner on demo page does not seem to work on iPhone 3GS and Android 2.1 update1. iPhone 4 is ok.
Matt Kocaj
on 23 Sep 10This is dog slow even on the iphone4. And if you zoom/pan the page a little, within a few seconds you’ll get an error state where all div elements seem to pulse in a synchronized manner. That’s a fail for me.
Additionally the CPU usage is clearly high. When you can’t open another tab (mobile Safari, again on the i4) without waiting like 15 seconds you know there’s something wrong.
I have been working with webkit CSS3 animations this week for a scaling project and discovered that you need to use the “3d” transitions to enable the built in hardware rendering in some webkit browsers (mobile safari/chrome). This made a massive rendering and CPU improvement for my project and seemed to resolve a lot of repaint/flicker issues i was getting on the i4 and ipad.
So i tried to modify your 2nd demo to use the 3d functions but for some reason it didn’t seem to improve the performance issues (same slow zoom/pan and New Tab behavior). Perhaps you can find something i have missed in the updated version to get it over the line. I’ve learned that where many changes are required to 3d functions, only missing one small change can fail the whole page to be rendered using the hardware enhancements.
JZ
on 23 Sep 10@Espen – that canvas example on StackOverflow has more in common with the first attempt that I rejected. It uses a PNG image (dataURI) and only spins the image. The ticks on the spinner are moving, not alternating. It’s just not the effect I was looking for.
@Matt and others citing CPU usage – the demo showing several spinners on one screen is an example of showing how it can be easily modified to look different. I can’t think of a case where you’d actually use that many on one screen and CPU load is very reasonable showing only one. This is true in the specific application this was made for, you’ll only see one spinner at a time.
@vanls – we’ve successfully tested the iPhone 3GS, but not Android 2.1. Are you using iOS 4?
Matt
on 24 Sep 10I think it’s an issue with iOS 3 (3.1.3 for sure). It works fine on my 3G with iOS 4.1 – but on an iPhone emulator for 3.1.3 it’s showing square spines rather than rounded and pulsing flashes rather than rotating.
I’ve seen similar issues with creating circle buttons where the button diameter is 30px and you set the corner radius to 15px or greater. On 4+ devices the buttons show as perfectly round, on 3.1.3 they show as squares.
This discussion is closed.