What are we making?
You may have seen some of these loaders, mostly on Flash websites. It’s basically a piece of pie getting bigger and bigger to become a whole circle.
At a first I thought it’s gonna be easy peasy, just have a circle, make it spin, hide some part of it behind a mask and I’m done. Well, it turned out it’s way more difficult. In fact, CSS is not prepared for such a task, even with preprocessors like Sass & Compass. We always struggle when it comes to making shapes, and even more when we have to style or animate those. Most of the time, we manage to work around it and get something working, at the price of maintainability or semantics.
Why do this?
I think the most common use case would be timers. But these concepts could be used to make pie charts with pure CSS as well. If you’re es
Even if there are several awesome tools out there to manage pie charts (mostly with JavaScript), we could probably easily figure out how to do pie charts with CSS only, and even animate those with such a trick.
There is a tutorial about making CSS pie charts with the clip property on Atomic Noggin Enterprise website.
Well, this is a workaround with bad semantics! But the maintainability isn’t so bad, so here we go.
The HTML
We need 3 different elements:
- a spinner: this is the half-circle which will rotate during the whole thing
- a mask: this is the element which will hide the spinner during the first 50% of the animation
- a filler: this is the element which will complete the circle during the last 50% of the animation
And we need all these elements to be in the same parent to allow absolute positioning:
<div class="wrapper">
<div class="pie spinner"></div>
<div class="pie filler"></div>
<div class="mask"></div>
</div>
Since the spinner and the filler are two half of the same circle, we use a shared class (.pie
) to style them.
To keep the code in this article clean and understandable, we won’t add any vendor prefixes.
The parent element sets up the size and absolute positioning context for the timer:
.wrapper {
width: 250px;
height: 250px;
position: relative;
background: white;
}
It is important to make sure the width and height are equal to make a circle and ensure the whole thing works.
The “spinner” and the “filler” share this CSS:
.pie {
width: 50%;
height: 100%;
position: absolute;
background: #08C;
border: 10px solid rgba(0,0,0,0.4);
}
Their width equals 50% of the parent width since they are both part of the same circle, and their height is the same as the parent height. We also give them some color and a border to identify them properly.
The “spinner”
.spinner {
border-radius: 125px 0 0 125px;
z-index: 200;
border-right: none;
animation: rota 10s linear infinite;
}
We have to make the thing look like a half-circle with border-radius on the top left corner and the bottom left corner. Plus, we give it a positive high z-index
in order to put it on top of the filler but behind the mask.
Then we add the animation
at 10 seconds long. We’ll talk more about animations later.
The “filler”
.filler {
border-radius: 0 125px 125px 0;
z-index: 100;
border-left: none;
animation: fill 10s steps(1, end) infinite;
left: 50%;
opacity: 0;
}
For the spinner, we set border-radius
and z-index
, remove the border-left
, and make the animation
10 seconds long. For this element, the animation-timing-function
is not set to linear
but to steps(1, end)
. This means the animation
will not progressivly go from 0% to 100% but will do it instantly.
Since the filler won’t be visible during the first part of the animation, we set its opacity to 0, and its position to 50% of the parent width.
The “mask”
.mask {
width: 50%;
height: 100%;
position: absolute;
z-index: 300;
opacity: 1;
background: inherit;
animation: mask 10s steps(1, end) infinite;
}
The mask is present since the beginning of the animation, so its opacity is set to 1 and its background is inherited from the parent background-color (to make it invisible). In order to cover the spinner, it has the same dimension has it, and its z-index is set to 300.
The keyframes
@keyframes rota {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fill {
0% { opacity: 0; }
50%, 100% { opacity: 1; }
}
@keyframes mask {
0% { opacity: 1; }
50%, 100% { opacity: 0; }
}
The first animation (rota
) is for the spinner. It progressively rotates from 0 to 360 deg in 10 seconds.
The second animation (fill
) is for the filler. It immediatly goes from 0 to 1 opacity after 5 seconds.
The last animation (mask
) is for the mask. It immediatly goes from 1 to 0 opacity after 5 seconds.
So the animation looks like this:
- T0 – the spinner is on the left, hidden by the mask. The filler is hidden.
- T1 – the spinner starts rotating clockwise, and slowly appears from behind the mask.
- T2 – the spinner has gone 360/10*2 = 72° and keeps rotating.
- T3 – the spinner has gone 360/10*3 = 108° and keeps rotating.
- T4 – the spinner has gone 360/10*4 = 144° and keeps rotating.
- T5 – the spinner has gone 360/10*5 = 180° and keeps rotating. At this very moment, the filler instantly goes at 100% opacity while the mask goes disappears.
- T6 – the spinner has gone 360/10*6 = 216° and keeps rotating.
- T7 – the spinner has gone 360/10*7 = 252° and keeps rotating.
- T8 – the spinner has gone 360/10*8 = 288° and keeps rotating.
- T9 – the spinner has gone 360/10*9 = 324° and keeps rotating.
- T10 – the spinner has gone 360°, getting back to its starting point. Then we restart the animation. The mask goes at 100% opacity while the filler goes disappears.
Bonus
Here’s some additional tricks which can be pretty cool, depending on what you want.
Pause on hover
.wrapper:hover .filler,
.wrapper:hover .spinner,
.wrapper:hover .mask {
animation-play-state: paused;
}
With this snippet of code, you can hover the whole animation by hovering the parent element.
Inside content
Thanks to z-index
, we can easily add some content inside the spinner and make it rotate the same way. Try adding the following snippet to your code :
.spinner:after {
content: "";
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
top: 10px;
right: 10px;
background: #fff;
border: 1px solid rgba(0,0,0,0.4);
box-shadow: inset 0 0 3px rgba(0,0,0,0.2);
}
Pre-processors or CSS variables
Currently, this isn’t very easy to maintain. But if we use variables (which preprocessors or the upcoming native CSS variable) we can make it easier. For example, you could add a variable to manage duration time, instead of having to change it on the 3 animation declarations.
If you want to ease the maintainability without dealing with pre-processors, I guess you could create a class which only handle the animation duration, and add this class to the 3 children elements. It would look like something like that :
.animation-duration {
animation-duration: 10s;
}
Downsides
Sadly, there are some things this technique can’t do:
- Doesn’t support gradients (looks awful)
- Doesn’t support box-shadows (looks dirty)
- Isn’t completly responsive. If you change the parent element’s dimension, everything goes right except the border-radius. You still have to change border-radius values manually because we can’t set 50% border-radius unless we’re dealing with a perfect square.
- Isn’t semantic (4 elements for a single animation). Upcoming multiple pseudo elements may help this, or web components.
Browser support
Since we are using CSS animations and keyframes, the browser support is pretty low but will improve over time. For now, the only browsers supporting CSS animations are:
- Internet Explorer 10
- Firefox 12+
- Chrome
- Safari 5+
- Opera 12+
- iOS Safari 3+
- Android 2+ (buggy till v4)
Demo
While this us quite cool as a demonstartion of the power of CSS, I am once again compelled to ask, why not SVG?
Make a demo!
Codepen: SVG Pie Timer
Based on the HSL spinner posted by Lars Gunther.
Actually, gradients do work, under certain conditions:
The borders mustn’t be transparent (as in the demo) and the gradients have to be radial (of course).
Also, the gradient on the
.filler
has to be left aligned (left center
) and the one on the.spinner
has to be right aligned (right center
).Also, if you tweak the animations a little, other things (maybe even box-shadows) should work, too (the opacity has to change instantly, which means
or something…)
I will make a demo soon :)
Now I had time to make the demo.
It demonstrates gradients and some more things, see for yourself!
Hey, really nice work Felix, I like it !
As I said in the article, I already knew we could put some content on it (refering to the “Click me” pseudo-element), and with the border-radius fix, I also knew the thing could be fully responsive. By the way, we could have use the transform: scale() property to make it bigger or smaller. ;)
But your work with gradients looks great ! I definitly need to dig in into radial-gradients. Anyway, thanks for making such a demo. :)
Hi ! Thanks Chris, this is an honor to see my work published on CSS Tricks. :)
Thanks to Joshua Hibbert, I finally figured out how to manage the responsive issue with border-radius.
That’s right, we can’t do such a thing :
But, we can use the "complex" border-radius property to achieve it, simply doing this :
This way, we don't have to change border-radius values if we want to make the whole loader bigger or smaller. :)
@Felix Kiunke : I'd love to see how you manage to deal with radial-gradients. Please, let me know if you make a demo. :)
To make the opacity changes instantly, you may want to use steps(1,start/end), I think it's work pretty well !
Great write up Hugo!
A CSS only spinner: http://codepen.io/tmyg/pen/bwLom
Webkit only for the moment, could probably be made to work in Firefox.
Spoiler alert…. :)
Just in case you want a visual to follow along with Hugo’s (excellent!) walk-through of what is all happening here. Changing a couple background-colors will do the trick.
Nice work, Hugo!
And maybe this is show in the post above, but for some reason a part of the post is giving me a “We’re sorry but something went wrong” error. Might be because I’m on my work laptop.
Thanks for that Keith!
That makes it a lot easier to make sense of how everything works. Very cool techniques used.
Chris, not exactly the same thing, and not as beautiful since I lack designer skills, but look at the inner circle in this SVG-based demo.
I’m yet to be sold on CSS3 animations. I love the fact that we can do this, but there are just too many negatives IMO.
The lack of browser support is obviously the major one, but as a developer rather than a designer I find Jquery based animation packages a lot ‘easier’ to use and interact with.
I think this is an awesome way to do things, I just can’t justify that much CSS to do something I can do in a few lines in JQ.
Brilliant blog by the way :)
I totally agree with you. It’s just for the fun (and challenge) of it. :)
Rich, I completely agree that doing something like this is, if you want to actually use it in a production website, is something that should be left up to Javascript and jQuery. But for a demo to find out what’s possible, this is very impressive.
For smaller things though, CSS animations are perfect. They degrade well, they’re part of the styling that you have to do anyway, and you get the accessibility pushers off your back because whatever you do doesn’t rely on javascript, and can still be seen by screen readers and the like.
For simple animations, it’s also definitely not any longer! It’s shorter if anything!
For example, to fade a navigation button’s background to a darker colour on hover, using the following HTML:
Using CSS Animations:
Using jQuery:
If you've got an unsupported browser, the button will just snap from one to the other using CSS Animations. if you've got an unsupported browser with jQuery, nothing happens!
It definitely has it's advantages, and small little touches like this are just what they're meant for!
You’re forgetting that CSSS animations are faster http://jsperf.com/jquery-animate-vs-css – the major gamechanger for me was switching to CSS when jQuery was causing severe lag on mobile devices.
Looks nice. But my CPU may feel hotter. It eats about 10%.
This was a great little tutorial thanks for sharing.
Hugo’s always two steps ahead of me. I’ve made a Transparent Dwindler (decrement spinner) that I’m working the kinks out of.
If you haven’t checked out CodePen please do. It’s in Beta, so don’t forget to offer your support.
Slightly different but equally effective,
Ludvig Lindblom made a slightly different but very clever version allowing linear-gradients on the circle : http://codepen.io/ludviglindblom/pen/dfjAt.
Actually, he makes the mask rotate instead of the spinner, and plays with z-index and opacity during the second half of the animation.
Be sure to have a look at it, it’s awesome as well. ;)
Don’t confuse this with the CSS3 PIE project. Google led me here by mistake!
I recently found a way to remove one of the 3 animations to do the pie timer.
The animation-direction property allow us to control the way the animation is played: forward or backward. Thanks to this, we don’t need 2 animations anymore for opacity control: only one, and the reverse value.
According to MDN:
So instead of using an animation going from 0 to 1 opacity, I use the one going from 1 to 0, and play it backward. So I removed the “fill” animation and set to the .fill element this:
Have a look at the demo to see the way it works: http://codepen.io/HugoGiraudel/pen/BHEwo
Always the craftsman.
When embedded into this page, this demo doesn’t not work in Chrome.
With a little bit of jquery, here I create a flexible pie countdown timer (you can change the totaltime). I hope it is useful for you :D