Build a CSS loading animation of three animated dots in this step-by-step guide.
Loading animations enhance the user experience by letting a user know something is loading on the site. They’re particularly good when used with an API-based application that needs to fetch information prior to displaying it on the page.
Our loading animation is inspired by Oleg Frolov’s Loading XXI design animation over on dribbble. We like the minimalist yet classy aesthetic which does a marvelous job communicating to a user their screen is loading.
Plus, Loading XXI gives a neat twist on the usual loading dot animations out there as it depicts a stream of dots that flow in and drop out.
Your end product will look something like this:
See the Pen Loading Dot Animation by Klea Merkuri (@thehelpfultipper) on CodePen.
1. Start with static text
Create a #preloader wrapper to hold the loading animation. Inside, nest a #text container to hold the text “Loading” and the dots that will act as animated ellipsis. This will be all the HTML we need.
<div id="preloader">
<div id="text">Loading</div>
<div class="wrapper" id="firstWrap"><div class="dot"></div></div>
<div class="wrapper" id="secondWrap"><div class="dot"></div></div>
<div class="wrapper" id="thirdWrap"><div class="dot"></div></div>
<div class="wrapper" id="fourthWrap"><div class="dot"></div></div>
</div>
Each dot is a <div>
with a class of .dot and each .dot is wrapped in a parent <div>
with a class of .wrapper. There are four .dot containers instead of three because we need the extra to create the illusion of a smooth playing animation.
Too bad the trick of using four <div>
s instead of three didn’t come until the fifth stylesheet attempt. We were really stumped on the best way to iterate infinitely over different keyframes without running into the dreaded regression of the sliding first and second dots (see reference design) at the end of each keyframe.
As a gentle reminder, experimentation and failure are expected when coding. Five stylesheets and one unnecessary javascript file later prove that perseverance is key.
Also, it might seem redundant to nest the dots in this fashion, but it’s an important step in the way we chose to animate the third dot. You’ll see what we mean later on!
2. Style the preloader container
Begin by adding styles to the body like a background color to make #preloader stand out.
Tip: Get rid of default browser styles on the document body like margins and paddings!
body {
margin: 0;
padding: 0;
height: 100vh;
background-color: rgb(150, 150, 150);
}
Set full height on the body so we can move #preloader in the center of the page vertically in the next step.
Then add styles to #preloader like so:
#preloader {
background-color: #F5F5F5;
text-align: center;
max-width: 800px;
height: 90vh;
border-radius: 10px;
position: relative;
top: 5%;
margin: 0 auto;
box-shadow: 0px 0px 5px rgb(215, 228, 228);
}
Notice that we vertically align #preloader by moving it 5% from the top relative to its parent, the body. This is possible because we assigned a height to the body. If you remove the height from the body, the relative position won’t take effect.
3. Shape the dots
First, enlarge the ‘Loading’ text so it resembles in size and style that of our reference design.
#text {
font-size: 280%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 800;
color: #222222;
letter-spacing: 0.04rem;
}
Second, give substance to the dots to fake the ellipsis. Remember each .dot container is wrapped in a .wrapper container. We’ll make the parent .wrapper identical to its child .dot by turning them into small circles.
.wrapper, .dot {
width: 9px;
height: 9px;
border-radius: 50%;
}
Nothing shows up on the page yet because our small circles hold no content and bare no color. Assign background-color:black
only to the <div>
s with the .dot class. The .wrapper containers shouldn’t have any color – they’re there solely for the animation part later on!
This is what you should have so far:
Positioning elements that stack on top of each other
We definitely need to fix the position of the elements. To stack each of the <div>
s on top of one another, set position as absolute, and move them from the top until they hit the midline and are level with each other.
Stacking here is important for the animation of the dots. We’d like to see only three dots the entire time, but we have (and need) four so two will always be stacked to fake the three.
So give #text position:absolute
with top:45%
and move .wrapper slightly further down so it’s on a baseline with ‘Loading’ :
.wrapper {
position: absolute;
top: 50.5%;
}
All that’s left is to move the elements leftward so they lie next to each other. Only two elements will remain stacked and those are the first two .dot containers.
To use the left positions throughout our stylesheet without having to repeat the number, we’ll assign custom CSS properties. It’s for convenience’s sake, you definitely don’t have to do this. But if you choose to, then set --left:30%
on the body.
Note: Convention has custom CSS properties assigned to :root, a pseudo-class that allows global access of those properties throughout the document. We chose to set them on the body though as it serves our purposes well enough.
Now set left:var(--left)
on #text. The var() function is how we access custom CSS variables. What this does is move #text 30% from the left.
Upon doing this we realized #text should be moved slightly further to the left which we easily managed using left:calc(var(--left) + 2%)
. Here the calc() function nicely takes care of the maths once we pass in the rule to calculate. We opted to move #text an additional 2% to the left.
As for the .dot containers, they’ll need to shift more than the value of –left. A little trial and error gave us the number 198px on top of the 30% shift. Considering we’ll shift the other two .dot containers even further from this initial shift, store the value into a custom property as well on the body, --init-wrap:calc(var(--left) + 198px)
.
Tip: You can call custom CSS properties anything you want (as long as you remember what you called them, LOL).
Don’t forget to actually assign left:var(--init-wrap)
to .wrapper!
4. How to fake ellipsis
For the first two .dot containers there’s no need to create a leftward shift since the initial left shift, –init-wrap, is sufficient and they need to remain stacked.
But the third and fourth .dot containers need to move as all we can see at the moment is a single, sad dot next to ‘Loading’.
First, decide the distance you want between the dots – we opted for 15px. Second, add your chosen distance to the –init-wrap value.
#thirdWrap {
left: calc(var(--init-wrap) + 15px);
}
#fourthWrap {
left: calc(var(--init-wrap) + 30px);
}
Note: #fourthWrap is shifted a total of 30px – 15px from –init-wrap where #firstWrap and #secondWrap are located (which puts it on top of #thirdWrap) plus 15px right from #thirdWrap.
You should now have your ellipsis impostors!
5. Adding animations to the dots
Stare at the reference intensely (okay, jk take it easy) to observe what exactly is happening.
We notice three things:
- the first two dots slide to the right
- a new dot takes up first place with a grow effect
- the last dot gets pushed out and falls in a curved line
Whew! It looked somewhat simple at first, but there are plenty of different actions taking place at the same time. No worries, we’ll tackle each of these animated aspects one by one.
Slide animation for a rightward shift
Two containers, #secondWrap and #thirdWrap, will slide using animation:slide 2s infinite
. The name of the animation is “slide” (it can be called anything you want) and it will last a total of 2 seconds. And by setting the third parameter, the iteration count, as infinite we make the animation play non-stop.
Of course, the animation property by itself does nothing other than set the stage for what should happen. We need to define the actions in a keyframe for the slide animation.
@keyframes slide {
0% {
transform: translate(0px, 0px);
}
50%,100% {
transform: translate(15px, 0px);
}
}
The move to the right of the two dots will occur at 50% of the animation’s run-time which is 2 seconds. So the slide animation takes, in fact, only 1 second to complete before it restarts again. The leftover second is a delay – think of it as the time the other dots take to complete their animations in a single cycle.
We didn’t specify the animation-delay property because it delays the start of the animation but doesn’t apply to every cycle afterward. Instead, we declare total seconds that account for the animation run-time plus the animation delay.
Tip: A 1 second animation with a 1 second delay makes for a total of 2 seconds. In the keyframe, 100% refers to that total 2 seconds. To run the animation for 1 second, or 1/2 the total seconds, we complete the shift in 1/2 * 100% = 50%. No tricky maths here, just smart ones 😉
Note: The Why of the Extra Dot
Comment out #firstWrap and take a look at the effect of the slide animation in the case of having only three containers.
Once the two dots move to the right, the keyframe ends and cycles again. But on that second cycle, the dots that shifted return to their starting position.
Clearly, we don’t want to see that happen because it’s not a smooth flow nor does it resemble the continuous stream of dots that allegedly come in and fall out.
Now remove the comment from #firstWrap and notice how the regression upon the end of the keyframe doesn’t exist to the eye. This is why we added that extra dot – makes life easier!
Grow effect for the first dot
Upon every slide of the first dots, the magical empty space that the first dot used to occupy produces another dot.
Similar to the slide animation, the grow animation will take 2 seconds and repeat infinitely.
#firstWrap {
animation: grow 2s infinite;
}
Note: Every animation will take a total of 2 seconds! The delays are incorporated in the total and defined in the keyframes.
To simulate the continuous stream of dots we use scale().
@keyframes grow {
0%, 50% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
In the grow animation, the delay occurs in the first second which is when the slide animation takes place. We want the first two dots to slide first, then have a new dot appear in the first place.
Drop effect for the last dot
One more dot to animate and this one is an interesting case that involves some creativity and lots of frustrating Google-ing.
For the last dot, two things occur:
- it gets kicked to the right by the second sliding dot, leading to a slight rightward shift
- it falls off in a curve
Getting an element to fall in a curved path ain’t for the faint of heart. There are various ways to achieve this, many far more involved in the maths and I encourage you to explore a few to get a better idea of things.
For this project, we kept it simple by using a hack to achieve a curve by separating the movement between the x and y axes. It’s why we needed to wrap the fourth .dot in particular in a parent container. As the parent moves in one direction, the .dot container will move in the other direction.
We’ll first tackle the slight rightward shift by adding animation:slideOnDrop 2s linear infinite
to #fourthWrap.
Note: The “linear” parameter defines the animation-timing-function and assures we animate at an even speed.
@keyframes slideOnDrop {
0%, 20% {
transform: translateX(0);
}
25% {
transform: translateX(11px);
}
100% {
transform: translateX(200px);
}
}
Around the halfway point of the 1 second of the slide animation, the slideOnDrop animation will kick in shifting the third dot 11px to the right. Then, it will proceed for the remaining time to shift 200px.
Note that we shifted #fourthWrap, the parent of the fourth .dot container, horizontally. Therefore, the .dot container moves horizontally along its parent.
Our objective is to separate the movement of #fourthWrap from its child .dot so .dot moves vertically while #fourthWrap moves horizontally for the interval between 25% to 100%.
#fourthWrap div {
animation: drop 2s ease-in infinite;
}
@keyframes drop {
0%, 25% {
transform: translate(0px, 0px);
}
100% {
transform: translate(0px, calc(90vh - 0.5*90vh));
}
}
The drop animation kicks in on the 25% mark until the total seconds are complete. Child .dot is shifted vertically on the y-axis at a distance that’s the equivalent of the bottom half of #preloader.
>> Tip <<
We made a small correction to account for the vertical positioning of the dots on smaller screen sizes.
Create a custom CSS property –top with the value of 45% and replace the top value of #text with var(–top). Then using –top as basis, position each .wrapper calc(var(–top) + 36px) from the top to prevent misalignment on smaller screens!
Add overflow:hidden
to #preloader to make the “disappearance” of the third dot seamless and we’re done!
The loading dot animation is complete, you can use it anytime you need to let a user know their page is loading. Find the full source code on GitHub and don’t forget to try our other projects!
More: How To Build An Interactive Menu Component