
Single Element Loaders: The Dots
Single Element Loaders: The Dots êŽë š
Weâre looking at loaders in this series. More than that, weâre breaking down some common loader patterns and how to re-create them with nothing more than a single div. So far, weâve picked apart the classic spinning loader. Now, letâs look at another one youâre likely well aware of: the dots.
Dot loaders are all over the place. Theyâre neat because they usually consist of three dots that sort of look like a text ellipsis (âŠ) that dances around.
Single-Element Loaders series:
Our goal here is to make this same thing out of a single div element. In other words, there is no one div per dot or individual animations for each dot.
That example of a loader up above is made with a single div element, a few CSS declarations, and no pseudo-elements. I am combining two techniques using CSS background and mask. And when weâre done, weâll see how animating a background gradient helps create the illusion of each dot changing colors as they move up and down in succession.
The background animation
Letâs start with the background animation:
.loader {
width: 180px; /* this controls the size */
aspect-ratio: 8/5; /* maintain the scale */
background:
conic-gradient(red 50%, blue 0) no-repeat, /* top colors */
conic-gradient(green 50%, purple 0) no-repeat; /* bottom colors */
background-size: 200% 50%;
animation: back 4s infinite linear; /* applies the animation */
}
/* define the animation */
@keyframes back {
0%, /* X Y , X Y */
100% { background-position: 0% 0%, 0% 100%; }
25% { background-position: 100% 0%, 0% 100%; }
50% { background-position: 100% 0%, 100% 100%; }
75% { background-position: 0% 0%, 100% 100%; }
}
I hope this looks pretty straightforward. What weâve got is a 180px-wide .loader element that shows two conic gradients sporting hard color stops between two colors each â the first gradient is red and blue along the top half of the .loader, and the second gradient is green and purple along the bottom half.
The way the loaderâs background is sized (200% wide), we only see one of those colors in each half at a time. Then we have this little animation that pushes the position of those background gradients left, right, and back again forever and ever.
When dealing with background properties â especially background-position â I always refer to my Stack Overflow answer where I am giving a detailed explanation on how all this works. If you are uncomfortable with CSS background trickery, I highly recommend reading that answer to help with what comes next.
In the animation, notice that the first layer is Y=0% (placed at the top) while X is changes from 0% to 100%. For the second layer, we have the same for X but Y=100% (placed at the bottom).
Why using a
conic-gradient()instead oflinear-gradient()?
Good question! Intuitively, we should use a linear gradient to create a two-color gradients like this:
linear-gradient(90deg, red 50%, blue 0)
But we can also reach for the same using a conic-gradient() â and with less of code. We reduce the code and also learn a new trick in the process!
Sliding the colors left and right is a nice way to make it look like weâre changing colors, but it might be better if we instantly change colors instead â that way, thereâs no chance of a loader dot flashing two colors at the same time. To do this, letâs change the animationâs timing function from linear to steps(1)
The loader dots
If you followed along with the first article in this series, I bet you know what comes next: CSS masks! What makes masks so great is that they let us sort of âcut outâ parts of a background in the shape of another element. So, in this case, we want to make a few dots, show the background gradients through the dots, and cut out any parts of the background that are not part of a dot.
We are going to use radial-gradient() for this:
.loader {
width: 180px;
aspect-ratio: 8/5;
mask:
radial-gradient(#000 68%, #0000 71%) no-repeat,
radial-gradient(#000 68%, #0000 71%) no-repeat,
radial-gradient(#000 68%, #0000 71%) no-repeat;
mask-size: 25% 40%; /* the size of our dots */
}
Thereâs some duplicated code in there, so letâs make a CSS variable to slim things down:
.loader {
width: 180px;
aspect-ratio: 8/5;
--_g: radial-gradient(#000 68%, #0000 71%) no-repeat;
mask: var(--_g),var(--_g),var(--_g);
mask-size: 25% 40%;
}
Cool cool. But now we need a new animation that helps move the dots up and down between the animated gradients.
.loader {
/* same as before */
animation: load 2s infinite;
}
@keyframes load { /* X Y, X Y, X Y */
0% { mask-position: 0% 0% , 50% 0% , 100% 0%; } /* all of them at the top */
16.67% { mask-position: 0% 100%, 50% 0% , 100% 0%; }
33.33% { mask-position: 0% 100%, 50% 100%, 100% 0%; }
50% { mask-position: 0% 100%, 50% 100%, 100% 100%; } /* all of them at the bottom */
66.67% { mask-position: 0% 0% , 50% 100%, 100% 100%; }
83.33% { mask-position: 0% 0% , 50% 0% , 100% 100%; }
100% { mask-position: 0% 0% , 50% 0% , 100% 0%; } /* all of them at the top */
}
Yes, thatâs a total of three radial gradients in there, all with the same configuration and the same size â the animation will update the position of each one. Note that the X coordinate of each dot is fixed. The mask-position is defined such that the first dot is at the left (0%), the second one at the center (50%), and the third one at the right (100%). We only update the Y coordinate from 0% to 100% to make the dots dance.

Hereâs what we get:
Now, combine this with our gradient animation and magic starts to happen:
Dot loader variations
The CSS variable we made in the last example makes it all that much easier to swap in new colors and create more variations of the same loader. For example, different colors and sizes:
What about another movement for our dots?
Here, all I did was update the animation to consider different positions, and we get another loader with the same code structure!
The animation technique I used for the mask layers can also be used with background layers to create a lot of different loaders with a single color. I wrote a detailed article about this. (afif) You will see that from the same code structure we can create different variations by simply changing a few values. I am sharing a few examples at the end of the article.
Why not a loader with one dot?
This one should be fairly easy to grok as I am using the same technique but with a more simple logic:
Here is another example of loader where I am also animating radial-gradient combined with CSS filters and mix-blend-mode to create a blobby effect:
If you check the code, you will see that all I am really doing there is animating the background-position, exactly like we did with the previous loader, but adding a dash of background-size to make it look like the blob gets bigger as it absorbs dots.
Note
If you want to understand the magic behind that blob effect, you can refer to these interactive slides (thebabydino) (Chrome only) by Ana Tudor (anatudor) because she covers the topic so well!
Here is another dot loader idea, this time using a different technique:
This one is only 10 CSS declarations and a keyframe. The main element and its two pseudo-elements have the same background configuration with one radial gradient. Each one creates one dot, for a total of three. The animation moves the gradient from top to bottom by using different delays for each dot..
Oh, and take note how this demo uses CSS Grid. This allows us to leverage the gridâs default stretch alignment so that both pseudo-elements cover the whole area of their parent. No need for sizing! Push the around a little with translate() and weâre all set.
More examples!
Just to drive the point home, I want to leave you with a bunch of additional examples that are really variations of what weâve looked at. As you view the demos, youâll see that the approaches weâve covered here are super flexible and open up tons of design possibilities.
Next upâŠ
OK, so we covered dot loaders in this article and spinners in the last one. In the next article of this four-part series, weâll turn our attention to another common type of loader: the bars. Weâll take a lot of what we learned so far and see how we can extend them to create yet another single element loader with as little code and as much flexibility as possible.
Single-Element Loaders series: