Grow your CSS skills. Land your dream job.

CSS Pie Timer

Published by Guest Author

Editor's Note: Hugo 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 he sent me a write up on how he did it and I'll be darned if it doesn't make a real nice CSS-Tricks article. Thanks Hugo!

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 outthere 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 : http://atomicnoggin.ca/blog/2010/02/20/pure-css3-pie-charts/

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 positionning :

<div class="wrapper">
  <div class="spinner pie"></div>
  <div class="filler pie"></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;
}
<div class="wrapper">
  <div class="spinner pie animation-duration"></div>
  <div class="filler pie animation-duration"></div>
  <div class="mask animation-duration"></div>
</div>

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

Comments

  1. While this us quite cool as a demonstartion of the power of CSS, I am once again compelled to ask, why not SVG?

  2. 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

    49.9% {opacity: 0}
    50% {opacity: 1}

    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. :)

  3. A CSS only spinner: http://codepen.io/tmyg/pen/bwLom

    Webkit only for the moment, could probably be made to work in Firefox.

  4. Permalink to comment#

    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:

      
      <style>
      .nav-list li {
         background: #fff;
         padding: 3px 8px;
         display: inline-block;
         }
      </style>
      
      <ul class="nav-list">
          <li>Home</li>
           <li>About</li>
         <li>Contact</li>
      </ul>
      

      Using CSS Animations:

      
      .nav-list li {
         transition: background .25s ease-in-out;
         -moz-transition: background .25s ease-in-out;
         -webkit-transition: background .25s ease-in-out;
      }
      
       .nav-list li:hover {
           background: #ddd;
         }
      

      Using jQuery:

      
      <script src="./jquery.min.js"></script>
      <script type="text/javascript" src="./jquery-ui.min.js"></script>
      <script type="text/javascript"> 
      $(document).ready(function(){ 
      $(".boxes").hover(function() { 
      $(this).stop().animate({ backgroundColor: "#ddd"}, 250); },function() { $(this).stop().animate({ backgroundColor: "#ffffff" }, 250)
      ; })
      ; });  
      </script>
      

      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.

  5. Looks nice. But my CPU may feel hotter. It eats about 10%.

  6. Permalink to comment#

    This was a great little tutorial thanks for sharing.

  7. Andre
    Permalink to comment#

    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.

  8. Permalink to comment#

    Slightly different but equally effective,

  9. Don’t confuse this with the CSS3 PIE project. Google led me here by mistake!

  10. sime
    Permalink to comment#

    When embedded into this page, this demo doesn’t not work in Chrome.

  11. Ela Yudhanira

    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

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".