CSS Pie Timer

Avatar of Kitty Giraudel
Kitty Giraudel on (Updated on )

Editor’s Note: Kitty Giraudel made a cool demo for a “pie timer” on CodePen. It’s definitely not an intuitive or simple thing to do in CSS. Then they sent me a write up on how they did it and I’ll be darned if it doesn’t make a real nice CSS-Tricks article. Thanks Kitty!

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.

 
The CSS

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

View Demo on CodePen