How to create a beautiful animated loader with nothing but CSS

Julien Benchetrit
codeburst
Published in
6 min readJul 24, 2017

--

Image by Itai Hanski

tl;dr An example of how creative you can be with pseudo-elements and keyframe animations. Used in this case to build a loader animation without the need to fetch any JS or images.

“Why?” you ask

This is, above all, an article about CSS, pseudo-elements, keyframe animations and how much one can do with these tools. I don’t believe in the need for a loader or splash screen in every web app out there and won’t be trying to convince you of that.

In my last homepage project, the very first section needed a video and a few images to load before it could be ready. I didn’t want to show the content immediately as the user might scroll down too soon and miss the full experience. But I did want to make sure they stuck around long enough for everything to load.

This is why I decided to build an animated loading screen that would appear ASAP and stay visible until the rest of the content was ready. In order to achieve this, it would need to be built without any external assets, only HTML and embedded CSS.

How I built it

This is where things diverge based on the design of the loading screen you may want to build. I’ll use mine as an example while trying to keep things general.

Note that this article assumes familiarity with pseudo-elements, CSS animation properties and keyframe animations. If you want a refresher on the latter two, you can find a great one here. And here’s everything you wish to know about pseudo-elements.

Ok, ready?

Getting started

Before deciding to build it with HTML and CSS only, I had a gif version:

Design by Avner Gicelter

It will serve as a useful baseline for our version. As you can see, there are four steps in the animation:

  1. The borders appear one by one.
  2. The red, orange, and white rectangles slide in.
  3. The rectangles slide back out.
  4. The borders disappear.

We only need to build steps 1 and 2. Using animation-direction: alternate; will reverse them and create steps 3 and 4 for us. We’ll also make the animation repeat forever using animation-iteration-count: infinite;.

Let’s start with the basic HTML structure:

<!doctype html>
<html>
<head>
<!-- <link rel="preload"> for CSS, JS, and font files -->
<style type="text/css">
/*
* All the CSS for the loader
* Minified and vendor prefixed
*/
</style>

</head>
<body>
<div class="loader">
<!-- HTML for the loader -->
</div>

<header />
<main />
<footer />
<!-- Tags for CSS and JS files -->
</body>
</html>

The CSS is embedded in the head and the loader HTML right after the opening body tag. Perhaps there’s a better way to take advantage of the browser’s critical rendering path? If you can see an improvement, please let me know in the comments.

Building the logo itself

Instead of directly analyzing the final version, we’ll try to follow the logical steps one would use to build a similar animation. In my head, that first step was building the logo without any animation.

div.logo serves as the parent square and each rectangle inside is represented by a div.{$color}:

<div class="logo">
<div class="white"></div>
<div class="orange"></div>
<div class="red"></div>
</div>

The red is after the orange which is after the white because, by default, elements are stacked from last to first. Each one is positioned absolutely from the side it will be appearing from (as in left for the red rectangle and bottom for the orange one) and given the appropriate height or width.

In SCSS, we start with:

div.logo {
width: 100px;
height: 100px;
border: 4px solid black;
box-sizing: border-box;
position: relative;
background-color: white;
& > div {
position: absolute;
}
div.red {
border-right: 4px solid black;
top: 0;
bottom: 0;
left: 0;
width: 27%;
background-color: #EA5664;
}
/* Similar code for div.orange and div.white */}

This is the result:

Animating the border

Still with me? Because here starts the tricky (and interesting) part.

CSS doesn’t allow us to directly animate div.logo’s border in the way we want. So we must remove the border from the square and find a different way to create it, one that can be animated.

Perhaps if we could break down the borders into pieces and make them appear sequentially? We could do that using two transparent pseudo-elements overlaying the full square.

Each one renders two of the four borders of the square. We can then make every border appear separately by animating each pseudo-element’s width and height from 0% to 100% in sequence.

Let’s try that! We’ll create a static version first. div.logo::before is positioned absolutely at the top left corner of div.logo and will represent its top and right borders. div.logo::after goes to the bottom right and shows the bottom and left borders.

Our SCSS for div.logo now looks like this:

div.logo {
width: 100px;
height: 100px;
box-sizing: border-box;
position: relative;
background-color: white;
&::before,
&::after {
z-index: 1;
box-sizing: border-box;
content: '';
position: absolute;
border: 4px solid transparent;
width: 100%;
height: 100%;
}
&::before {
top: 0;
left: 0;
border-top-color: black;
border-right-color: black;
}
&::after {
bottom: 0;
right: 0;
border-bottom-color: red; // Red for demo purposes only
border-left-color: red;
}
}

And the result:

Next, let’s animate div.logo::before with our first keyframe.

For the initial state, the pseudo elements now have width and height set to 0. We use the keyframe to animate width to 100% and, right after that, height to 100%.

Along with switching the border colors from transparent to black at the right moment, this causes the top and right borders to animate exactly the way we want them to.

The code, including the changes to the pseudo-elements’ initial state:

div.logo {
&::before,
&::after {
/* ... */
width: 0;
height: 0;
animation-timing-function: linear;
}
&::before {
/* ... */
animation: border-before 1.5s infinite;
animation-direction: alternate;
}
}
@keyframes border-before {
0% {
width: 0;
height: 0;
border-top-color: black;
border-right-color: transparent;
}
24.99% {
border-right-color: transparent;
}
25% {
height: 0;
width: 100%;
border-top-color: black;
border-right-color: black;
}
50%,
100% {
width: 100%;
height: 100%;
border-top-color: black;
border-right-color: black;
}
}

We repeat the same step for div.logo::after without forgetting to adjust the timing and invert width and height. We now have the full border animation:

Animating the rectangles

Finally, we need to animate the rectangles.

The main challenge is that we can’t chain keyframes. We need to schedule each animation to take into account the steps inside all the others. That way, the overall animation will appear sequential. And it can be reversed.

For the border animation, we had simply given 25% of the time to each border. We need to add the rectangles this time. After some trial and error, we choose the following to happen over 1.5 seconds:

  1. 0 to 25%: Top and right borders appear
  2. 25 to 50%: Bottom and left borders appear
  3. 50 to 65%: Red rectangle appears
  4. 65 to 80%: Orange rectangle appears
  5. 75 to 90%: White rectangle appears

Using the above timeline, we can now write the keyframe to animate our red rectangle’s opacity and width:

div.logo {
div.red {
/* ... */
width: 0;
animation: red 1.5s infinite;
animation-direction: alternate;
}
}
@keyframes red {
0%,
50% {
width: 0;
opacity: 0;
}
50.01% {
opacity: 1;
}
65%,
100% {
opacity: 1;
width: 27%;
}
}

We repeat the same steps for the orange and white rectangles and obtain our final result:

Thanks for reading! I’m Julien, front-end engineer at Healthy.io, a digital health startup. If you want any clarifications or think I skipped something important, please let me know and I’ll make sure to fix it.

✉️ Subscribe to Codeburst’s once-weekly Email Blast, 🐦 Follow Codeburst on Twitter, and 🕸️ Learn Full Stack Web Development.

--

--