Last week I was tasked with improving <paper-spinner>, a Polymer element that implements the so-called “circular activity indicator” in Google’s Material Design spec. It has a simple yet slick animation, and at first glance it may seem simple to create one of these spinners using purely web technologies (no GIFs allowed!). However, once you actually start trying to build one, you’ll realize it’s difficult to get all the growing/shrinking, rotating, and color animations to be on the right timing, not to mention making the spinner perform well, even when there’s heavy JavaScript computation happening at the same time (you are displaying a spinner to show the user that something is happening, right?).

Exhibit A - The Fancy, CSS-Animated SVG Spinner

The first spinner was created by a colleague a while ago, and I used it as a starting point to build my spinner. It animates the stroke-dashoffset (i.e. where the dashed-border begins) of a custom SVG path for the growing/shrinking animation, and rotates the the entire path in step intervals to make it appear as if the arc starts growing from where it shrank to at the previous iteration. The path’s stroke color is animated from blue to red to yellow to green, and the container <span> is rotated at a different rate to make the animation appear random. With relatively little work, I was able to get this spinner working on Chrome, Safari and Firefox.

This is a purely CSS animations - no JavaScript needed. However, this spinner does not work on Internet Explorer since IE does not support CSS animations for SVG elements. Also, some of the CSS properties that are animated, such as stroke and stroke-dashoffset, are expensive to animate since they will trigger the browser to repaint. This is especially apparent if you Show paint rectangles in Chrome DevTools, or use the toggle button below to do some heavy JavaScript computations:

Doing heavy JavaScript causes the repaints to block, which means the spinner will be much slower.

Exhibit B - The IE-Compatible, JS-Animated SVG Spinner

To make the spinner compatible on Internet Explorer, I had to move to JavaScript animations. Using <core-animation> (which uses web-animations-js), I was able to move most of the animation declarations from CSS to HTML. The only exception was CSS transform, which IE does not support for SVG elements. So instead, I wrote a custom animation effect which sets the transform attribute on the <path> element (remember that SVG elements have this transform attribute, but not regular HTML elements).

rotateSpinnerEffect: function(timeFraction, target, animation) {
  // IE does not support CSS transforms on SVG elements, so we use SVG transforms instead.
  target.setAttribute('transform',
    'translate(14, 14) rotate(-' + (timeFraction * 360) + ') translate(-14, -14)');
},

Even though this spinner looks great on all the browsers, it’s still not a great spinner. Why? It’s perhaps even worse than the first spinner in terms of performance, since JavaScript must be executed to perform the animation (try toggling the heavy JavaScipt button above again and look at this spinner).

Exhibit C - The GPU-Accelerated, Clipped Semi-Circles Spinner

I needed a new way to animate the growing/shrinking arc, so I searched for “animate circle filling css”. I was inspired by this example, which simulated the effect by rotating semi-circles in and out of view. The semi-circles are just a <div> with border-radius: 50% and transparent bottom and left borders. However, this example does has some shortcomings:

Instead of using CSS clip, I decided to put the rotating circle into a <div> with overflow: hidden, which has the same effect and is cross-browser compatible. I also opted to animate the two semi-circles from the middle out, which means I don’t have trigger any repaints.

To animate the color transitions, I created semi-circles for each of the four colors and animated the CSS opacity property for each one. That way, it can be GPU-accelerated.

The end result? A spinner that is fast and doesn’t trigger any repaints. Here’s some examples of <paper-spinner> in use, and how it can be customized.

Update (Nov 14, 2014): After the original implementation was published, observant readers have discovered that as the spinner was rotating, there is a small but noticable transparent gap in the middle of the arc. This is because in many browsers (including Chrome 38, Safari 7.1, and IE 11), the gap between two adjacent display: inline-block; overflow: hidden; elements is visible while the container is playing a rotation animation (Firefox is the only browser that doesn’t have this issue).

To overcome this unfortunate rendering issue, I added a “patch” (another div.circle) to cover up the gap. The difference can be observed below (the spinner on the right has the patch).