Creating Custom Preloaders with CSS animations & keyframes

As Javascript developers, we are often quickly dismissing CSS. It's tempting to let one of the many Javascript frameworks take care of our animations. Yes, CSS is complex, messy and don't even get me started on browser compatibility. Yet, there is no way around dealing with CSS on a daily basis if you're a frontend developer (or aspiring to become one). Unlike many other parts of CSS, animating elements on your website is a pretty straightforward process and not as complicated as you might think. Note that CSS animation is not supported in IE 10 and lower (it's not as bad as it sounds, we still cover about 98% of all internet users).

In this post we will explore the foundations of CSS animations and create our custom animation using keyframes. Instead of just writing about the theory, I thought it would be more interesting to have some real-world examples, that you can follow along. So we will create an animated preloader that you can implement on your next website.

Prerequisites: This guide will assume that you've already used CSS to style elements on websites before. You should feel comfortable with basic rules such as background-color, opacity and border-radius. You should also feel confident with CSS-selectors. If you need to brush up your targeting with CSS-selectors, CSS-diner by Luke Pacholski is a great resource to level up your skills.

Paint, Composition and Layout

Before we actually start working with code, let's take a second to look at the theory behind CSS animations. Why do we care? Because Animations can be very resource intensive. We have to avoid slowing down the browser on the clients machine at all cost. Only then we can ensure smooth animations.

Your browser renders CSS in three steps. The first step is the layout operation. During the layout operation, your browser will calculate the geometry and position of an element depending on its height, width, floats and other properties. As you can imagine, this requires quite some work. Your browser has to consider the impact on other divs and the entire layout of your website. We call this an expensive operation. After the geometry has been established and an area of the website was dedicated to our new (or changed) element, the element will be painted. This operation takes care of colorizing the previously created area. Finally, after the painting is finished, the changes will be reintegrated with the rest of the page, causing an update of your composition.

The trick for high-performance CSS-animations is an easy one: Find properties that trigger either only a composition or composition & painting update. Try to avoid layout changes through animation. How to know which CSS-rules are triggering certain updates? Have a look at CSS triggers by Paul Lewis. This table gives you an overview on how each rule triggers paint / composition and layout updates. Pay attention to properties like opacity and transform, they count to the few rules that (depending on the browser) require no layout operation. Perfect for our CSS animations.

Creating the Preloader

First, let's have a quick glimpse at the finished preloader we are going to build:

What a beauty. Now in the first step, let's create the HTML part of our preloader.

<div class='preloader-dots'>  
  <div class='dot'></div>
  <div class='dot'></div>
  <div class='dot'></div>
  <div class='dot'></div>
  <div class='dot'></div>
</div>  

Nothing special here. Let's add some basic static CSS styling to these classes.

.preloader-dots {
  height: 50px;
  width: 80px;
}

.preloader-dots .dot {
  display: inline-block;
  background: #666;
  height: 8px;
  width: 8px;
  opacity: 0.3;
  border-radius: 50%;
}

As you can see below, this gives us five boring, static circles. We can't impress our users with that.

Let's create our first keyframe animation.

@keyframes up-and-down {
  50% {transform: translateY(-20px);}
}

Before we look into the whole keyframe syntax, let's look at this part of the code transform: translateY(-20px). Transform is triggering changes in the shape or positioning of your element. Most importantly, as we have established in the CSS-triggers list, transforms are rather inexpensive and allow smooth transitions. In this example, we are using translateY(), in combination with transform. This will cause our element to move on the Y axis. Either up (negative values) or down (positive values). In this case, we are moving our element 20px up.

Now to the interesting part. The @keyframe rule allows us to specify events in a range from 0% to 100%. Between the events that you specify, the values will transition smoothly. In our case, we are modifying the value transform:translateY. The default value at 0% is transform:translateY(0px). It will then be gradually moved down until it reaches transform: translateY(-20px), at 50% of the animation. After this point at 50%, the animation will turn the value back up until it again reaches the default value transform:translateY(0px) at 100%.

Now we still need to assign our keyframe to the class of our element.

.preloader-dots .dot {
  display: inline-block;
  background: #666;
  height: 8px;
  width: 8px;
  opacity: 0.3;
  border-radius: 50%;
  animation: up-and-down 2s infinite;
}

@keyframes up-and-down {
  50% {transform: translateY(-20px);}
}

Take a look at the animation rule we've just added to .preloader-dots .dot. First we've added the name of our @keyframe animation up-and-down. You could rename this animation to whatever fits your style. Then we proceed by giving a duration for the entire animation from 0% - 100%. For now, we've specified 2 seconds. This means our dots will reach the top (-20px) after 1 second, and will have returned to the bottom after 2 seconds have elapsed. The final attribute infinite will ensure that our animation isn't stopped after its first iteration. Instead of infinite we could also run the animation only once or a specific number of times. There are several other attributes you could add to the animation rule. You could reverse the direction of the animation or change the speed curve. For an entire documentation of the animation rule, it'd be worth having a look at the w3schools documentation. Now let's have a look at the current state of our animated preloader:

It's moving but all of our dots are moving at the same time. We need to add a bit of delay between each dot. We can achieve this by using animation-delay for each of our dots.

.preloader-dots .dot:nth-child(2) {
  animation-delay: .15s;
}

.preloader-dots .dot:nth-child(3) {
  animation-delay: .3s;
}

.preloader-dots .dot:nth-child(4) {
  animation-delay: .45s;
}

.preloader-dots .dot:nth-child(5) {
  animation-delay: .6s;
}

The second dot will wait 0.15 seconds before beginning the animation, the third will wait 0.30 seconds and so forth. If you want an even animation, you should pay attention that the delay between each dot is the same (0.15s in the example).

Still doesn't quite look like the final result. We need to add some more parts to our keyframe animation. We want the dots to stay down for a while after they have bounced up. Also, we want them to go a little lower than their initial position, to make it a bit more bouncy.

@keyframes up-and-down-final {
  0% {
    transform: translateY(0px);
  }
  35% {
    transform: translateY(0px);
    opacity: 0.3;
  }
  50% {
    transform: translateY(-20px);
    opacity: 0.8;
  }
  70% {
    transform: translateY(3px);
    opacity: 0.8;
  }
  85% {
    transform: translateY(-3px);
  }
}

From 0-35% of the animation, the dot will remain on the 0px position. Then, from 35-50% it will move up. Until 70% the dots will fall down again, even a bit lower than their initial position, until 85% it'll rise again, just a little bit higher than their initial position and finally return to their 0px default position. I've also added a few opacity changes, to make the colors jump a little. We could potentially also change the background-color of all the dots or each dot individually.

Here is the entire code again:

.preloader-dots .dot {
  display: inline-block;
  background: #666;
  height: 8px;
  width: 8px;
  opacity: 0.3;
  border-radius: 50%;
  animation: up-and-down 2s infinite;
}

.preloader-dots .dot:nth-child(2) {
  animation-delay: .15s;
}

.preloader-dots .dot:nth-child(3) {
  animation-delay: .3s;
}

.preloader-dots .dot:nth-child(4) {
  animation-delay: .45s;
}

.preloader-dots .dot:nth-child(5) {
  animation-delay: .6s;
}

@keyframes up-and-down {
  0% {
    transform: translateY(0px);
  }
  35% {
    transform: translateY(0px);
    opacity: 0.3;
  }
  50% {
    transform: translateY(-20px);
    opacity: 0.8;
  }
  70% {
    transform: translateY(3px);
    opacity: 0.8;
  }
  85% {
    transform: translateY(-3px);
  }
}

I hope this post helped you to understand the idea behind CSS animations and keyframes. Now get creative and build your own preloader. Here is another one I've built:

You can check out the code for this preloader and a collection of other preloaders that I have built with exactly the same simple css rules in my codepen collection.

Further down the rabbit hole