Regis Gaughan, III

Google/Gmail’s 3D Loader

The 3D loading circle in pure CSS with only a single element

About a year ago, I set out to recreate Google/Gmails’s 3D iOS flipping circle loader. It was a success at the time but relied on JavaScript to trigger the states and, at some point, stopped working flawlessly. I wanted to revisit it now to see if we could be recreate it using only CSS and a single HTML element… Good news: We can!

If you’re on most current browsers you should see the 3D-Page-Flipping-Color-Changing-Circle-Animation Google uses when Gmail and Chrome are loading content.

Want to jump right in? Check out the Github repo now.

The Setup & Layout

The goal for this project is to create a Google/Gmail’s 3D Loading Circle using a single element. For this purpose we will use a single span element with a class of “gloader.”

<span class="gloader"></span>

Okay, so we’ve got our single element. Although, I suppose you could say that calling our single span a single element is cheating since we actually have three elements to play with: the span itself as well as its ::before and ::after pseudo-elements.

So, can we use just these three elements? Yes! First, let’s setup our three style-able “elements” correctly:

  1. Our span element will be the “base” and rounded to a circle using border-radius. It will also be given a position:relative so it can hold it’s two pseudo-elements within.

  2. Speaking of, our ::before element will be forced into a block element, turned into a semi-circle and absolutely positioned to the right half of our base circle. It is this element that will be the “next” color to be revealed.

  3. Finally, we’ll take our ::after element and style it to be nearly the same layout as — but on top of — our ::before semi-circle. It is this element that will flip.

.gloader { display:inline-block; width:1em; height:1em; position:relative; overflow:hidden; border-radius:0.5em; } .gloader::before, .gloader::after { content:' '; display:block; width:50%; height:100%; overflow:hidden; position:absolute; top:0%; left:50%; z-index:1; border-radius:0em 0.5em 0.5em 0em; } .gloader::after { z-index:2; }

The Animation

Okay, now things are going to start to get a little more complex. To animate these, we will need three animations going on at once. Essentially, we are going to recreate the following series of animations:

  1. We have our base styled in green, with our ::before semi-circle positioned to the right styled to be blue (or next color). We then have our ::after on top of that styled greenonce again. To the viewer, we have a solid green circle.

  2. We begin to “flip” our ::after up towards the center, transitioning to dark green for a shadow. Since our blue ::before semi-circle is underneath it, it begins to become seen.

  3. Once our “flip” reaches the top, we immediately switch it’s color to dark blue, again, for the shadow.

  4. We then seamlessly continue to “flip” our ::after over the left half of the circle from it’s dark blue color to the brint blue color.

  5. When we reach the bottom on the left, the viewer now sees a solid blue color.

  6. Now, we swap the base to be solid blue, rotate the ::before semi-circle so it is on the left side covered by the now blue ::after semi-circle and change it to our next color of bright red. Since we want the flip to come from the top, we need to rotate the base 90 degrees.

  7. Now, just like in #5, the viewer still sees a solid blue color. Repeat #2-#6 for all the colors infinitely.

Alright, so our animations should look something like this:

@keyframes base { 0% {transform:rotate(0deg); background-color:#21aa29;} 25% {transform:rotate(90deg); background-color:#2159d6;} 50% {transform:rotate(180deg); background-color:#d62408;} 75% {transform:rotate(270deg); background-color:#ffcf00;} 100% {transform:rotate(360deg); background-color:#21aa29;} } @keyframes under { 0% {background-color:#2159d6; transform:rotate(0deg);} 25% {background-color:#d62408; transform:rotate(180deg);} 50% {background-color:#ffcf00; transform:rotate(0deg);} 75% {background-color:#21aa29; transform:rotate(180deg);} 100% {background-color:#2159d6; transform:rotate(0deg);} } @keyframes flip { /* This is very verbose, we'll optimize it later */ 0% {background-color:#21aa29; transform:rotateY(0deg); } /* Bright Green */ 12.5% {background-color:#105514; transform:rotateY(90deg); } /* To Dark Green */ 12.51% {background-color:#102c6b; transform:rotateY(90deg); } /* Quick Switch to Dark Blue */ 25% {background-color:#2159d6; transform:rotateY(180deg);} /* To Blue */ 37.5% {background-color:#102c6b; transform:rotateY(90deg); } /* To Dark Blue */ 37.51% {background-color:#6b1204; transform:rotateY(90deg); } /* Quick Switch to Dark Red */ 50% {background-color:#d62408; transform:rotateY(0deg); } /* To Red */ 62.5% {background-color:#6b1204; transform:rotateY(90deg); } /* To Dark Red */ 62.51% {background-color:#7f6700; transform:rotateY(90deg); } /* Quick Switch to Dark Yellow */ 75% {background-color:#ffcf00; transform:rotateY(180deg);} /* To Yellow */ 87.5% {background-color:#7f6700; transform:rotateY(90deg); } /* To Dark Yellow */ 87.56% {background-color:#105514; transform:rotateY(90deg); } /* Quick Switch to Dark Green */ 100% {background-color:#21aa29; transform:rotateY(0deg); } /* Back to Bright Green */ } /* And apply these to our elements */ .gloader {animation:base 3s steps(1) 0s infinite;} .gloader::before {animation:under 3s steps(1) 0s infinite;} .gloader::after {animation:flip 3s linear 0s infinite; transform-style:preserve-3d;} .gloader::before, .gloader::after {transform-origin:0px 50%;}

Okay, there might be a couple things you notice here.

  • The first are that we want our base and under animations to trigger at each keyframe and not animate between each keyframe. Luckily, we can use the steps() timing function when we assign our animations. Using steps(1), as we have, is essentially saying to only have one “step” between each keyframe which gives us the effect we want. Without this, the span and ::before elements would continually rotate in circles and would probably cause the viewer to become dizzy and fall over.

  • The second thing you may notice is out transform-origin for the two pseudo-elements. By default, the origin of a css transformation is the center point of the elements’ bounding box. However, since our two semi-circles are only half the width, and are start on the right side our base we need to change that origin to be the center of the left edge, or transform-origin:0px 50%.

  • We’ll also give our “flipping” element a transform-style:preserve-3d since we want it to appear three dimensional. Visually, I don’t see much difference though.

Optimization

The only thing we can do further is optimize our “flip” animation. The first optimization we can make, is having just two rotation declarations. Since our we want our flip to occur four times in total, we can have a single transform change from 0deg at 0% to 720deg at 100%.

@keyframes flip { 0% {background-color:#21aa29; transform:rotateY(0deg);} 12.5% {background-color:#105514;} 12.5% {background-color:#105514;} 12.51% {background-color:#102c6b;} 25% {background-color:#2159d6;} 37.5% {background-color:#102c6b;} 37.51% {background-color:#6b1204;} 50% {background-color:#d62408;} 62.5% {background-color:#6b1204;} 62.51% {background-color:#7f6700;} 75% {background-color:#ffcf00;} 87.5% {background-color:#7f6700;} 87.56% {background-color:#105514;} 87.56% {background-color:#105514;} 100% {background-color:#21aa29; transform:rotateY(720deg);} }

Now, we can optimize these duplicate colors:

@keyframes flip { 0% {background-color:#21aa29; transform:rotateY(0deg);} 12.5%, 87.56% {background-color:#105514;} 12.51%, 37.5% {background-color:#102c6b;} 25% {background-color:#2159d6;} 37.51%, 62.5% {background-color:#6b1204;} 50% {background-color:#d62408;} 62.51%, 87.5% {background-color:#7f6700;} 75% {background-color:#ffcf00;} 100% {background-color:#21aa29; transform:rotateY(720deg);} }

We could make a couple more optimizations for browsers that support cascading styles within keyframes, but it is not standard and not widely supported. So we’ll stop here.

So, we finally put it all together:

.gloader { display:inline-block; width:1em; height:1em; position:relative; overflow:hidden; border-radius:0.5em; animation:base 3s steps(1) 0s infinite; } .gloader::before, .gloader::after { content:' '; display:block; width:50%; height:100%; overflow:hidden; position:absolute; top:0%; left:50%; z-index:1; border-radius:0em 0.5em 0.5em 0em; transform-origin:0px 50%; } .gloader::before { z-index:2; transform-style:preserve-3d; animation:flip 3s linear 0s infinite; } .gloader::after { animation:reveal 3s steps(1) 0s infinite; } @keyframes base { 0% {transform:rotate(0deg); background-color:#21aa29;} 25% {transform:rotate(90deg); background-color:#2159d6;} 50% {transform:rotate(180deg); background-color:#d62408;} 75% {transform:rotate(270deg); background-color:#ffcf00;} 100% {transform:rotate(360deg); background-color:#21aa29;} } @keyframes reveal { 0% {background-color:#2159d6; transform:rotate(0deg);} 25% {background-color:#d62408; transform:rotate(180deg);} 50% {background-color:#ffcf00; transform:rotate(0deg);} 75% {background-color:#21aa29; transform:rotate(180deg);} 100% {background-color:#2159d6; transform:rotate(0deg);} } @keyframes flip { 0% {background-color:#21aa29; transform:rotateY(0deg);} 12.5%, 87.56% {background-color:#105514;} 12.51%, 37.5% {background-color:#102c6b;} 25% {background-color:#2159d6;} 37.51%, 62.5% {background-color:#6b1204;} 50% {background-color:#d62408;} 62.51%, 87.5% {background-color:#7f6700;} 75% {background-color:#ffcf00;} 100% {background-color:#21aa29; transform:rotateY(720deg);} }

Browser Support

In the “final” version above browser support is only for IE 10+ and Firefox 16+. However, we can add support for Chrome 26+, Safari 6.0+ and Opera 12.1+ by simply adding the “-webkit-” prefixes for all transform, animation and keyframe css.

Now, if we want to broaden our support for older versions of Chrome and Safari — specifically Chrome 4+ and Safari 4.0+ — we can do so by loosening our requirements. Because of a long withstanding issue in webkit, pseudeo-elements could not be animated until recently. So, to support these older webkit versions we have to break our single-element goal and create two children spans inside our base element, and changing our ::before CSS rules to :first-child and our ::after rules to :last-child.

Finally, we can broaden down to Firefox 5+ by adding “-moz-” prefixes for all transform, animation and keyframe css and broaden to Opera 12.0+ by doing the same with an “-o-” prefix. Unfortunately, IE is stuck with a minimum version of 10.

All of these versions can be found in the Github repo.