In this tutorial we'll show you how to make some creative css-only loading animations also known as activity indicators.

Today, we are going to do a lot of stuff with CSS animations. Indeed, we will talk about CSS loading animations. What do you say? Ready?

A few things before we start:

You won’t see any vendor prefixes in the CSS snippets, but you will, of course, find them in the files.

The goal of the tutorial is to show the potential of CSS, particularly CSS3, that’s why the rendering could be altered on IE9-. If you plan to support these browsers, be sure to make fallbacks.

I personally use the box-model where [width] = [element-width] + [padding] + [borders]. I activate it with the following snippet:

*, *:before, *:after { box-sizing: border-box; }

CSS Stuff: Pros and Cons

What are the advantages and disadvantages of pure CSS loaders and preloaders (and more commonly CSS stuff)? Why not a JS solution, or even the good ol’ way: an animated GIF? There is no magic answer for this, it will depend on the situation. But let me give you some ideas.

Pros

CSS is easily editable: you can quickly change the duration, speed, color or whatever you want on your animation

CSS is “vector-like”: you can scale it as you wish without any quality loss

CSS animations are faster than JS “animations” (which are not exactly animations) since they use the native browser engine

CSS animations use GPU acceleration: if you have a good device, you’ll have very fast and smooth animations

CSS animations can be easily paused with the animation-play-state property

Cons

CSS animations don’t have full browser support: IE9- and Opera Mini don’t understand them

CSS (pre)loaders can involve heavy markup, not necessarily worth the cost

CSS animations can’t be easily fired on pointer-events (except hover) with JavaScript

One way to go would be to detect support with Modernizr, use CSS animations for browsers that support them and fall back to a regular GIF in other browsers. Win win.

Example 1

We will start with something pretty simple as always. Our little sphere is running infinitely from left to right in the bar. Movement is important to make the user understand the application/website is actually doing something.

The Markup

<div class="bar"> <i class="sphere"></i> </div>

Pretty minimalist, right? We could even do this we a single element but it can easily make the CSS ugly. Note that we could have both, clean markup and nice CSS when we’ll be able to animate pseudo-elements. 😉

The CSS

First, we create the container for the sphere: the bar. To preserve proportions and make the preloaders scalable, I used the em unit. Simply change the font-size value on the wrapper to scale the whole thing as you wish. Pretty cool, right?

.demo-1 .bar { /* Size and position */ font-size: 20px; /* 1em */ width: 10em; height: 1em; position: relative; margin: 100px auto; /* Styles */ border-radius: .5em; /* Height/2 */ background: white; /* Fallback */ background: rgba(255,255,255,0.6); box-shadow: 0 0 0 .05em rgba(100,100,100,0.075), /* Subtle border */ 0 0 0 .25em rgba(0,0,0,0.1), /* Outter border */ inset 0 .1em .05em rgba(0,0,0,0.1), /* Inset shadow */ 0 .05em rgba(255,255,255,0.7); /* Slight reflection */ }

Let’s immediately deal with the “Please wait.”. You’ve probably noticed it doesn’t appear in the markup: that’s because it’s generated content. Actually it should be in the markup in a real live case, since it’s important content, not just graphical stuff. But for the demo, I thought it could be enough to generate it.

.demo-1 .bar:after { /* Content and position */ content: "Please wait."; position: absolute; left: 25%; top: 150%; /* Font styles */ font-family: 'Carrois Gothic', sans-serif; font-size: 1em; color: #555; text-shadow: 0 .05em rgba(255,255,255,0.7); }

So in case you want to make this a real element, just create a span or whatever, give it those styles and it’s done. Now, let’s take a look at the sphere.

.demo-1 .sphere { /* Size */ display: block; width: 1em; height: 100%; /* Styles */ border-radius: 50%; background: linear-gradient(#eee, #ddd); box-shadow: inset 0 .15em .1em rgba(255,255,255,0.3), /* Top light */ inset 0 -.1em .15em rgba(0,0,0,0.15), /* Bottom shadow */ 0 0 .25em rgba(0,0,0,0.3); /* Outter shadow */ /* Animation */ animation: move 1.75s ease-in-out infinite alternate; }

Last but not least, the keyframes running the animation:

@keyframes move { to { margin-left: 90%; } }

How could it be easier, right? So the sphere element runs the move animation in 1.75 seconds alternatively from start to end and end to start.

Note: an easy way to convert this preloader into a progress indicator would be to remove the alternate parameter in the animation declaration, and set the according animation time based on the time you want the user to wait.



If you want to dynamically update the position of the element based on the progression of the loading, you’d have to couple this with a little bit of JavaScript of course.

Example 2

Now let’s continue with something a little bit more complicated. But not too much! A rotating … candy? Or something? Honestly, I don’t know. 😀

The Markup

Okay, so this one is entirely pseudo-element based. Completely. No need for extra markup.

<div class="spinner"></div>

The CSS

This will be quick, trust me. First, the element itself. I used red and beige, but you could pick whatever color you like. Same for the number of colors, I picked two but you could go with four. Or just one. Or any other.

.demo-2 .spinner { /* Size and position */ font-size: 100px; /* 1em */ width: 1em; height: 1em; position: relative; margin: 100px auto; /* Styles */ border-radius: 50%; background: #FF4F72; /* Fallback */ background: linear-gradient(#ea2d0e 50%, #fcd883 50%), /* First column */ linear-gradient(#fcd883 50%, #ea2d0e 50%); /* Second column */ background-position: 0 0, /* Position of 1st column */ 100% 0; /* Position of 2nd column */ background-size: 50% 100%; /* Contraction of "50% 100%, 50% 100%" */ background-repeat: no-repeat; box-shadow: inset 0 0 0 .12em rgba(0,0,0,0.2), /* Inner border */ 0 0 0 .12em rgba(255,255,255,0.1); /* Outter border */ opacity: 0.7; animation: rota 3s infinite alternate; }

Now, a pseudo-element for the inner transparent white circle.

.demo-2 .spinner:after { /* Size */ content: ""; width: 50%; height: 50%; /* Perfect centering */ position: absolute; top: 25%; left: 25%; /* Styles */ border: .12em solid rgba(255,255,255,0.3); border-radius: inherit; }

And the animation.

@keyframes rota { 25% { transform: rotate(270deg); } 50% { transform: rotate( 90deg); } 75% { transform: rotate(360deg); } 100% { transform: rotate(180deg); } }

Example 3

Now let’s dig into something a little bit more tricky. But don’t worry, it’s really not that hard. Actually, I tried to simulate random behavior in CSS. Long story short: it’s impossible unless you set completely weird values, and it’s still not random, of course. Anyway, we’ll see that later.

The Markup

The HTML for this one is not very pretty. Since we can’t animate pseudo-elements, we have to use multiple elements to do this. I went with a list (even if it’s not very semantic) but you could go with whatever you want.

<ul class="spinner"> <li></li> <li></li> <li></li> <li></li> </ul>

The CSS

First thing to do: style the list itself.

.demo-3 .spinner { /* Size and position */ font-size: 100px; /* 1em */ width: 1em; height: 1em; margin: 100px auto; position: relative; /* Styles */ list-style: none; border-radius: 50%; border: .01em solid rgba(150,150,150,0.1); /* Subtle white line circling the dots */ }

And the shared properties for all list elements.

.demo-3 .spinner li { width: .2em; height: .2em; position: absolute; border-radius: 50%; }

And now, a few explanations about what’s coming. When the page loads, the four dots are not overlapping each other, they are positioned like cardinal points: north, south, east, west. But, their rotation centers are all in the same spot: the exact center of the spinner.

.demo-3 .spinner li:nth-child(1) { background: #00C176; /* Blue */ top: 0; left: 50%; margin-left: -.1em; /* Width/2 */ transform-origin: 50% 250%; animation: rota 1.13s linear infinite, opa 3.67s ease-in-out infinite alternate; } .demo-3 .spinner li:nth-child(2) { background: #FF003C; /* Red */ top: 50%; right: 0; margin-top: -.1em; /* Height/2 */ transform-origin: -150% 50%; animation: rota 1.86s linear infinite, opa 4.29s ease-in-out infinite alternate; } .demo-3 .spinner li:nth-child(3) { background: #FABE28; /* Yellow */ bottom: 0; left: 50%; margin-left: -.1em; /* Width/2 */ transform-origin: 50% -150%; animation: rota 1.45s linear infinite, opa 5.12s ease-in-out infinite alternate; } .demo-3 .spinner li:nth-child(4) { background: #88C100; /* Green */ top: 50%; left 0; margin-top -.1em; /* Height/2 */ transform-origin: 250% 50%; animation: rota 1.72s linear infinite, opa 5.25s ease-in-out infinite alternate; }

Last, the two animations. One is for the rotation and one for the opacity. Yeah, the opacity is changing as well!

@keyframes rota { to { transform: rotate(360deg); } } @keyframes opa { 12.0% { opacity: 0.80; } 19.5% { opacity: 0.88; } 37.2% { opacity: 0.64; } 40.5% { opacity: 0.52; } 52.7% { opacity: 0.69; } 60.2% { opacity: 0.60; } 66.6% { opacity: 0.52; } 70.0% { opacity: 0.63; } 79.9% { opacity: 0.60; } 84.2% { opacity: 0.75; } 91.0% { opacity: 0.87; } }

Note: it occurred to me some people are uncomfortable with counter-clockwise rotation. Plus, clockwise reinforces the idea of the fast loading.

What’s wrong with you and numbers?

As I told you I tried to make things appear the more “random” I could. I could have built up a little JS script running random numbers and inserting them in CSS, but I don’t like the idea.

The best idea I had to do it was to generate random numbers from my head so it “looks like random”. That’s why you can see animation durations like 1.72s, 4.29s or 1.13s. Same things with keyframes from opa and opacity values. Weird intervals, weird values, “pseudo random”.

What about transform-origin?

I take the opportunity to give you a little trick to find the appropriate transform-origin value when you want to do a rotation which is not performed around the default rotation center.

The main problem I faced with the transform-origin property is the lack of comprehension of the value. I didn’t understand well (and sometimes still don’t) the way it handles the transform origin.

Anyway, the idea is to show the transform origin with a pseudo-element. Have a look:

.my-element { transform-origin: 12% 34%; } .my-element:after { content: ""; width: 4px; height: 4px; position: absolute; left: 12%; /* First value of transform-origin */ top: 34%; /* Second value of transform-origin */ margin: -2px 0 0 -2px; border-radius: 50%; background: red; }

Example 4

Let’s continue with a soft demo, a little bit steam punk: wheels, gears and such. I like this. Okay, let’s go.

The Markup

Pretty heavy on this one as well since we have to wrap each letter on a span. And we need a wrapper to cancel the main element rotation. We’ll talk about this later.

<div class="wrapper"> <div class="inner"> <span>L</span> <span>o</span> <span>a</span> <span>d</span> <span>i</span> <span>n</span> <span>g</span> </div> </div>

The CSS

First, we have many things to apply to the main element, such as size, position, font-styles, animation, etc.

.demo-4 .wrapper { /* Size and position */ font-size: 25px; /* 1em */ width: 8em; height: 8em; margin: 100px auto; position: relative; /* Styles */ border-radius: 50%; background: rgba(255,255,255,0.1); border: 1em dashed rgba(138,189,195,0.5); box-shadow: inset 0 0 2em rgba(255,255,255,0.3), 0 0 0 0.7em rgba(255,255,255,0.3); animation: rota 3.5s linear infinite; /* Font styles */ font-family: 'Racing Sans One', sans-serif; color: #444; text-align: center; text-transform: uppercase; text-shadow: 0 .04em rgba(255,255,255,0.9); line-height: 6em; }

Wow, that was pretty heavy, wasn’t it? We still have to create the inner wheels with pseudo-elements.

.demo-4 .wrapper:before, .demo-4 .wrapper:after { content: ""; position: absolute; z-index: -1; border-radius: inherit; box-shadow: inset 0 0 2em rgba(255,255,255,0.3); border: 1em dashed; } .demo-4 .wrapper:before { border-color: rgba(138,189,195,0.2); top: 0; right: 0; bottom: 0; left: 0; } .demo-4 .wrapper:after { border-color: rgba(138,189,195,0.4); top: 1em; right: 1em; bottom: 1em; left: 1em; }

Now the inner wrapper and the spans. Note how we use the reverse parameter on the inner container to cancel the main element rotation.

.demo-4 .wrapper .inner { width: 100%; height: 100%; animation: rota 3.5s linear reverse infinite; } .demo-4 .wrapper span { display: inline-block; animation: placeholder 1.5s ease-out infinite; } .demo-4 .wrapper span:nth-child(1) { animation-name: loading-1; } .demo-4 .wrapper span:nth-child(2) { animation-name: loading-2; } .demo-4 .wrapper span:nth-child(3) { animation-name: loading-3; } .demo-4 .wrapper span:nth-child(4) { animation-name: loading-4; } .demo-4 .wrapper span:nth-child(5) { animation-name: loading-5; } .demo-4 .wrapper span:nth-child(6) { animation-name: loading-6; } .demo-4 .wrapper span:nth-child(7) { animation-name: loading-7; }

Sadly, we need one animation for each letter since they need to be delayed. At first I thought about animation-delay, but this property only delays the first run, not each of them so it won’t work.

@keyframes rota { to { transform: rotate(360deg); } } @keyframes loading-1 { 14.28% { opacity: 0.3; } } @keyframes loading-2 { 28.57% { opacity: 0.3; } } @keyframes loading-3 { 42.86% { opacity: 0.3; } } @keyframes loading-4 { 57.14% { opacity: 0.3; } } @keyframes loading-5 { 71.43% { opacity: 0.3; } } @keyframes loading-6 { 85.71% { opacity: 0.3; } } @keyframes loading-7 { 100% { opacity: 0.3; } }

Those animations can be a little tricky to understand, so let me put it simple. Each letter needs to:

Lose a little bit of opacity Go back to full opacity Wait until every other letter has done the same Go back to step 1 and repeat

How do we do that exactly?

You count the number of letters in your element. Our demo counts 7. You divide 100 (the number of keyframes, expressed in %) by this number. In this example, it equals to ~14.28. Every 14.28 keyframes, a letter runs its thing. Done.

Example 5

Let’s end with something a little bit more conceptual. I know people won’t like it much, but depending on your website/application, you might consider this as an inspiration.

The Markup

Despite what it looks like, we only need one single element to achieve this.

<div class="pre-loader"></div>

The CSS

Actually, our element is only one of our little spheres (the one at the top). The seven others are made of box-shadows.

.demo-5 .pre-loader { /* Size and position */ font-size: 30px; /* 1em */ width: 1em; height: 1em; position: relative; margin: 100px auto; /* Styles */ border-radius: 50%; background: #123456; transform-origin: 50% 250%; animation: blink 1s steps(1, start) infinite, /* Blink */ counter-clock 8s linear infinite; /* Rotation */ /* Dots, clockwise */ box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em #123456, 0em 4em #123456, -1em 3em #123456, -2em 2em #123456, -1em 1em #123456; }

The transparent square effect is made with a … well, a transparent square. It’s a rotated pseudo-element put on top of everything else. Pretty easy.

.demo-5 .pre-loader:after { /* Size and position */ content: ""; width: 3em; height: 3em; position: absolute; left: -1em; top: 1em; /* Styles */ transform: rotate(45deg); background: white; /* Fallback */ background: rgba(255,255,255,0.6); }

And now the animations. A few explanations about the second one ( blink ):

We have 8 bullets, so we divide 100 keyframes into 8: 12.5.

Every 12.5 keyframes, a bullet loses a little bit of opacity. rgb(18,52,86) is the RGB code for #123456.

is the RGB code for #123456. On the first frame (12.5%), it’s the element itself that decreases opacity.

@keyframes counter-clock { to { transform: rotate(-360deg); } } @keyframes blink { 12.5% { background: rgba(18,52,86,0.6); box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em #123456, 0em 4em #123456, -1em 3em #123456, -2em 2em #123456, -1em 1em #123456; } 25% { background: #123456; box-shadow: 1em 1em rgba(18,52,86,0.6), 2em 2em #123456, 1em 3em #123456, 0em 4em #123456, -1em 3em #123456, -2em 2em #123456, -1em 1em #123456; } 37.5% { background: #123456; box-shadow: 1em 1em #123456, 2em 2em rgba(18,52,86,0.6), 1em 3em #123456, 0em 4em #123456, -1em 3em #123456, -2em 2em #123456, -1em 1em #123456; } 50% { background: #123456; box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em rgba(18,52,86,0.6), 0em 4em #123456, -1em 3em #123456, -2em 2em #123456, -1em 1em #123456; } 62.5% { background: #123456; box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em #123456, 0em 4em rgba(18,52,86,0.6), -1em 3em #123456, -2em 2em #123456, -1em 1em #123456; } 75% { background: #123456; box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em #123456, 0em 4em #123456, -1em 3em rgba(18,52,86,0.6), -2em 2em #123456, -1em 1em #123456; } 87.5% { background: #123456; box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em #123456, 0em 4em #123456, -1em 3em #123456, -2em 2em rgba(18,52,86,0.6), -1em 1em #123456; } 100% { background: #123456; box-shadow: 1em 1em #123456, 2em 2em #123456, 1em 3em #123456, 0em 4em #123456, -1em 3em #123456, -2em 2em #123456, -1em 1em rgba(18,52,86,0.6); } }

Note: I know I said counter-clockwise rotations are not that good, but since we couple it with a very fast clockwise animation, it’s a bit different this case.

Ease the process with external tools

As you may have seen in some demos, sometimes we need to repeat the same snippets of code for different values or something and it can be very annoying to do.

Thankfully, we can dramatically ease the process with appropriate tools. I think you’ve figured out this is the moment where I’m talking about CSS preprocessors. Indeed, but not only preprocessors.

Since the 4th demo was clearly the most repetitive, let’s try to ease the making of it for, let’s say, automatizing the process for changing the number of letters in the element.

Lettering.js

Lettering.js does a very simple thing: it wraps every letter in the targeted element with span tags.

So from now on, we don’t have to manually wrap our letters with spans, Lettering.js does it for us. Pretty neat, right? But we’re more concerned about the CSS part, I think.

CSS Preprocessors

You may have already understood it: we need a loop. And I have some good news, CSS preprocessors provide loops support! At least some of them…

Yeah, because you see, Sass does it the right way: the for loop. LESS however doesn’t provide a loop support. Instead, you have to make a recursive function decrementing a variable. It sucks, but it works.

/* SCSS */ for $i from 1 through 10 { /* Do stuff */ } /* LESS */ .loop(@index) when (@index > 0) { /* Do stuff */ .loop(@index - 1); } .loop(0) { } .loop(10);

See? In what universe is a recursive function easier than a for loop? Anyway, let’s make our 4th demo with “loops”.

/* SCSS */ $iterations : 7; //Number of letters @for $i from 1 through $iterations { $val : 100%/$iterations*$i; @keyframes loading-#{$i} { #{$val} { opacity: 0.3; } } .wrapper span:nth-child(#{$i}) { animation-name: loading-#{$i}; } } /* LESS */ @iterations: 7; //Number of letters @newline: `"n"`; //Fix hack .loop (@index) when (@index > 0) { @val: 100/@iterations*@index; (~"@keyframes loading-@{index} {@{newline}@{val}%") { opacity: 0.3; } (~"} .wrapper span:nth-child(@{index}) @{newline}") { animation-name: "loading-@{index}"; } .loop(@index - 1); } .loop (0) {} .loop (@iterations);

The output will be exactly the same as the one you read in the 4th demo. So it may be trivial when you have only 7 letters, but when you have 40 or 50, it can be a real time saver.

Note: adding prefixes to the SCSS version is pretty straightforward: duplicate the animation-name line, and the @keyframes block and add prefixes. However it’s completely painful with LESS and I’ll save you some time by showing you the end result.

If you want to understand how LESS concatenation works, and how you can handle things like @media or @keyframes in a mixin, please refer to the following discussions on Stack Overflow:

Final words

And this is already the end of the tutorial, alas! Let’s finish with a few links as further readings and resources:

Thanks for reading and be sure to share any related resource!