Jenning Ho
Posted on June 12, 2021
Let's talk about a set of CSS loaders / spinners that I've made recently. A little brief on the idea and the technical details behind them.
Here is the set of loaders and their source code, written in plain HTML & CSS. The idea is to make them readable to anyone with some CSS understanding and hopefully inspire you to make one yourself.
They are rearranged for better display, not sequentially ordered.
The brief
With this set, I wanted to do some motion animation with 2 dots mixed with more advanced and tricky animation using 3d perspective, that relies a bit on sleight of hand principle for the visual to work.
The first and the main constraint I have is to make these with single HTML element. This is so that they can be easily applied to projects without complicating the code too much, which makes them practical.
Secondly without applying SCSS or SASS, I can't be doing a lot of hardcoded values, or multiple nth elements with loop. I mostly mitigate this constraint by using CSS custom properties. CSS custom properties is different from SASS variables, in the sense that they are not static. They are dynamic based on context, which you'll see from their application in this set. A good example is --loader-size-half
applied in loader--7
. By only setting --loader-size
, --loader-size-half
being an abstract calc
function based on --loader-size
, will also update its value accordingly.
This set contains mostly square and circles, as such they are always same height and width. Instead of setting both width and height manually, I choose to use the relatively new CSS property aspect-ratio
. By only setting the width
value, and setting aspect-ratio
value to 1 / 1
, it turns the element into square. Finally, border-radius
50% will make any element that has equal width and height into circles.
The .loader class
.loader {
--loader-size: calc(var(--block-size) / 2);
--loader-size-half: calc(var(--loader-size) / 2);
--loader-size-half-neg: calc(var(--loader-size-half) * -1);
--light-color: rgba(255, 255, 255, 0.3);
--dot-size: 5px;
--dot-size-half: calc(var(--dot-size) / 2);
--dot-size-half-neg: calc(var(--dot-size-half) * -1);
display: block;
position: relative;
width: var(--loader-size);
display: grid;
place-items: center;
color: white;
}
This is the base class for all loaders. Within this class I've declared a few CSS custom properties that can be applied and reused in every loader.
.loader {
--loader-size: calc(var(--block-size) / 2);
--loader-size-half: calc(var(--loader-size) / 2);
--loader-size-half-neg: calc(var(--loader-size-half) * -1);
--light-color: rgba(255, 255, 255, 0.3);
--dot-size: 5px;
--dot-size-half: calc(var(--dot-size) / 2);
--dot-size-half-neg: calc(var(--dot-size-half) * -1);
}
- base loader size is half of block size, the container
-
--loader-size-half
is acalc
function that calculates half of loader size, which can be updated in real time by setting loader size value. -
--loader-size-half-neg
is simply half of loader size turned negative by multiplying it with -1. - light color is meant to be a constant to be used in many places.
- dot size is the size of each tiny white circle, is set to 5px.
The elements inside each loader are centered by default with this 2 lines:
.loader {
display: grid;
place-items: center;
}
The color of all elements is also set in this base class with color: white
, which sets the value of currentColor
and be used in every other classes.
position: relative
is set so that child elements' (::before and ::after pseudo elements) position: absolute
are anchored to it.
The dots
They are made with each loader element's pseudo elements, ::before
and ::after
. With each HTML element, we actually have 2 child elements and 1 parent element to work with. A lot of the time we can create these presentational elements with pseudo elements. You can read more about them in MDN.
You will notice that most other elements are set to vmin
value rather than px
value, but dot is different. vmin
values are directly tied to the viewport size, while px
is not. Ideally the dot size should be vmin
too, but why is it not?
This is mainly to make its position precise in relative the the line that each dots is "on". Ultimately when rendered on screen, the visuals are rendered in px
and the smallest px
on screen is 1px
. The line is a static 1px
size, and as such dot has to be too to make it "sit" on the line centered precisely. You can change dot's 5px
value to any vmin
value to see what I mean.
The loaders with dots can work with 4px ~ 6px sizes visually. So why 5px? So that it can be placed centered to the line. ie. 2px on top, 1px on the line centered, 2px at the bottom. This is the kind of calculation we should make to create pixel perfect layout.
The animation
A set of keyframes is declared for each loader despite some of them are the same set of keyframes to keep them separated and contained. Within each loader, the keyframes are reused by applying CSS custom properties. You can see them in action in loader 5, 6, 8, 9.
Most animation used in this set of loaders are simple translating (moving along the x, y, z axis) and rotating, but made interesting / unique via different easing and delay settings.
The subjects of each animation (ie. the dots) are actually doing the same motion, just one earlier than the other. This is achieved by simply setting a animation-delay
to one of the dot. Instead of setting a positive delay, which will cause one of the element to pause for awhile before animating, I applied a negative delay. This allow the element to act earlier instead of later, and so skipping the initial pause while also creating the difference in delay.
When it comes to easing, I am not actually a wizard with bezier-curve calculation. I first have an idea of the outcome, ie. "snappy", "bouncy", "smooth, etc, then play with them in the devtool easing editor to come up with a suitable easing for the animation. ie. for loader 7, the effect is achieved by having an overly "bouncy" easing that allows the dot to go out of it's linear curve and comes back. While most other loaders simply have a "snappy" motion.
Conclusion
With this, I have shared my thoughts and some technical details behind creating this set of loaders. I hope you can pick up some css trickery from this and inspire you to make your own. Feel free to use any of this in your project too. Would make me happy to see actual application of this set. Happy crafting!
Posted on June 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.