Radial progress indicator using CSS

How to use CSS3 transitions to create an animated radial progress indicator

http://jsfiddle.net/andsens/6e3QJ/

A colleague of mine recently had the task to implement a design that incorporated radial progress bars. The design was brilliant, the implementation worked (he used geedmos solution), and so he moved on to implementing the rest of the design.Somehow my thoughts got stuck with that progress indicator. I thought to myself “surely, you can make that indicator spin, maybe even with CSS transitions”. So I stubbornly set out to figure out how to do this.

My research on the net left me wanting, nobody really solved it.Yes, Jeff Pihach made a “radial progress bar”, but he uses CSS animations rather than CSS transitions. Except pausing and resuming it, you cannot do much with a CSS animation, there is no way to stop it at a predetermined location — like say a third through the animation to set the progress to 33%.Thereare jQuery plugins which can do it for you, but they use Javascript — CSS will always have the upper hand performance-wise when it comes to animations, especially on mobile devices.This is why I set out to implement my own CSS-animated radial progress bar.

Although the title of this post states that the progress indicator is made with CSS, we will actually use less.The advantages are manifold; primarily we use it because it will be easier to show the calculations used to position elements (later on we will also use it to create some nifty [data-progress=”@{progress}”] selectors).

The mask has the same CSS properties as “fill”, with the transition and background-color properties being the exception. Since we only want to show the half-circle on the right side of the circle, we mirror the clipping rectangle of the half-circle:

All we need to do is make the visible area of the left mask line up with the edge of the right half-circle.

In a static context of 0º (or 0%) this is easily achieved by rotating the left mask 180º. The right half-circle is behind its mask (i.e. on the left side) and the visible area of the left mask is on the right side of the circle lining up just like we intended.

At 0º, both visible areas of the left and right mask are on the right side of the circle — both half-circles are rotated behind the masks though and not visible (i.e. they’re on the left side of the circle). This makes sense of course, we’re at 0%, so we are not supposed to see anything but the circle background.

0%. The visible area of both masks is on the right side while the half circles are on the left side.

At this point, is there a difference between the left and right elements?After all: their masks and the corresponding visible areas are in the same position and so are the half-circles.Nope, there isn’t! Let’s hastily cement our enlightenment in code by removing the selector for the left half.

That code will make sure that our right half-circle and left mask always line up.

Here’s the interesting part: Now that the left half-circles parent mask is rotated its absolute rotation is suddenly doubled. At 100% both the left mask and the half-circle would be rotated 180º. Together that makes for 360º rotation. In contrast, the right half-circle still only rotates 180º (its parent mask is not rotated).

Stitching two half circles together to form a full circle

In light of those nausea-inducing element-whirls, let us rename the half-circles to “half-spin” and “full-spin”.

Mind the Gap

The gap between two clipped and rotated elements that are positioned right next to each other.

Because the browser needs to round to whole pixels you will be able to notice the “seam” between the two half-circles. The best solution I have been able to come up with is adding a third “fill-fix” that rotates together with the two half-circles.

This “fill-fix” is a child element of the static half-spin mask. It will sufficiently cover the gap if we set its full rotation to 360º. At 100% the “fill-fix” will overlap completely with the full-spin half-circle and not cover the gap, that specific position is however completely vertical for the clipping edges, so no rounding errors occur.

The same CSS as before applies, but we will have to adjust our Javascript:

Now we have a shadow on the bottom right of the inset, which will need to be balanced by one coming from the top left of the circle border. The problem here is however, that we do not have any elements overlaying our two masks, so we will need to add a shadow element.

This change could open up for a lot of new selectors we might want to add (like changing the color of the circle after 50%). In fact, we will be doing something in that regard later on.

However, writing those selectors will be a pain, we would have to define every selector from [data-progress=”%0”] to [data-progress=”%100”] and set the rotations accordingly.Luckily less can loop using recursive calls.

Animate All the Things!

The result is splendid (if I may say so myself). But we can go even further: Why should the percentage text not be animated as well?! It shouldn’t be a slider that simply moves the next number into place, but rather a quick switch from one number to the next.

This is where CSS gets complicated. Its transformations were built for smooth animations between two states and not abrupt switches, so how can we achieve with CSS animations exactly what they are trying to avoid?The answer is line breaks.

Line breaks move entire elements onto the next line once there is no space for them on the current one. We can abuse that behavior to animate our percentages. All we need is an element with all our numbers on one line, a container that only shows the first number on the next line, and an animation that widens or narrows the element depending on the percentage we set.

Why a container instead of a mask like in our previous half-circle endeavors you ask? The side-scroll would become enormous, because clipping an element only reduces its visible area, not the actual size of it.

With those three above mentioned elements in place the percentage that corresponds to the circle size will always be the first number on the second line during animation.

For starters we adjust the markup to contain all numbers from 0%-100% inside a new “numbers” element.

The first empty <span> is necessary so we can get 0% to jump to the second line as well.

We will need to move the numbers element one line-height up to have the second line be in the center of the inset. Also, we only want to show that second line (and only the first number), so we set the height of the outer percentage element to be that of the percentage font-size. Lastly we want to animate width changes in the numbers element.

The span elements inside “numbers” should be centered and have a constant width to ensure that each number jumps to the next line at the right time. Since we are displaying it as an inline-block element we need to fiddle with the vertical alignment.

Going further

There are various paths we could take to extend the indicator.Like implementing more than one half-circle to show colored segments or changing the color of the entire circle depending on the percentage.

The variables in less are not only useful to illustrate how things fit together, you can easily adjust them to make the inset larger and only have a slim progress indicator.

The easing function is also adjustable, have a look at how you can specify your very own timing function to make the animation behave exactly the way you like.

Instead of forking the various fiddles I linked to throughout this post, you can also look at everything on github maybe even fork the repo and improve upon my methods.

About the Author

Secoya A/S is located in Aarhus, Denmark

Anders Ingemann works for Secoya A/S, a company dedicated to providing advanced knowledge management solutions. In his spare time he rock climbs in the danish mountains and saves orphanages from supervillains. He is the author of the open source projects homeshick and bootstrap-vz.