Recently, I was working on an internal project and started thinking about the infinity symbol. After reading Will's great post on recreating the Archer title sequence with CSS animations, I came up with the idea to create a loader using the symbol. A loader is an animation used to signal to the user that something is happening, like data loading or when submitting a form.
A Loader Needs an Animation
My idea for the animation was to animate a small ball to follow the inside of the infinity symbol. I found this great CSS infinity symbol on CSS Tricks, courtesy of Nicolas Gallagher. Initially I tried to accomplish this using CSS by updating the positioning of the ball, but I quickly found that this was very tedious and not that smooth. I decided it was time to reach out to Will and see if he had any advice.
Let’s Get the Ball Rolling
When Jake came to me with the infinity loader idea I was excited to
jump in and give him a hand. Looking at the shape of the infinity
symbol, I focused on the fact that it is essentially two circles
with a small cross section in the middle that straightens out to connect them.
Looking at it this way, it makes sense to move the ball using transform: rotate()
and transform-origin
.
As a test I set up the ball to make the left circle. The ball is 20px to match the width
of the symbol's border. I'm using position: absolute
with the top
set at 50% and left
at 0.
There's also a margin-top
of -10px, which is half of the ball's height, to center the ball.
The transform: rotate()
is set to start at 0deg. The transform-origin
has an x-offset of 50px,
which matches the infinity symbol's border-radius
, and a y-offset of 10px, which is half of the
ball's height. These offsets position the ball's rotation point on the center of the left circle.
.ball {
height: 20px;
width: 20px;
border-radius: 50%;
background: #fff;
position: absolute;
top: 50%;
left: 0;
margin-top: -10px;
z-index: 10;
transform: rotate(0deg);
transform-origin: 50px 10px;
animation: infinity 2s linear infinite;
}
@keyframes infinity {
to {
transform: rotate(360deg);
}
}
Adjusting transform: rotate()
and transform-origin
to move our ball works well and is going
to simplify our movement significantly compared to manipulating the actual positioning.
We will still need to modify the positioning to move the ball to the right side.
Let’s take a look at that now and make the right circle as well.
Slide to the Right
For moving the ball over to the right side, we'll switch from using left
to right
positioning.
This makes the most sense, since the position would stay the same regardless of the
width of the infinity symbol. To do this we set the left
position to auto and the right
position
to 0. We’ll also need to set the transform-origin
x-offset to -30px so that the rotation is positioned
properly for the right side of the symbol. To get -30px we take the original 50px offset we used on the
left side and subtract the width of the ball, since we're moving the offset negatively for the right side.
@keyframes infinity {
0% {
transform: rotate(0deg);
transform-origin: 50px 10px;
left: 0;
}
50% {
transform: rotate(360deg);
transform-origin: 50px 10px;
left: 0;
}
/* switch sides */
50.1% {
left: auto;
right: 0;
transform: rotate(0deg);
transform-origin: -30px 10px;
}
100% {
left: auto;
right: 0;
transform: rotate(360deg);
transform-origin: -30px 10px;
}
}
After a bit of browser testing though, Jake noticed that something wasn’t working properly in
Firefox and Internet Explorer. Turns out those browsers don't switch the
positioning from left
to right
during the animation. Instead, they keep the left
positioning and the ball makes the
second circle further over to the left. To fix this, we'll have to stick with using left
positioning. Not a big deal.
So now we have our ball making both the left and right circles properly in all browsers.
@keyframes infinity {
0% {
left: 0;
transform: rotate(0deg);
transform-origin: 50px 10px;
}
50% {
left: 0;
transform: rotate(360deg);
transform-origin: 50px 10px;
}
50.1% {
left: 192px;
transform: rotate(0deg);
transform-origin: -30px 10px;
}
100% {
left: 192px;
transform: rotate(360deg);
transform-origin: -30px 10px;
}
}
Making the Connection
Things are coming along nicely now, so let's join the two circles. We want to start our ball in the center, so let’s first flip our starting rotation to 180deg.
Now we need to nudge it over to the right a bit. To do that we are going to stick with
the other property we’re animating and set our transform-origin
x-offset to 58px.
To get 58px I nudged the x-offset until the ball looked centered. Here's
a more mathematical approach if you'd prefer. Our infinity symbol halves have a diameter
of 100px and the total width including the cross section is 212px.
If we subtract the two circles from our total width we're left with 12px.
Our ball is 20px, and 20px minus 12px leaves us with an additional 8px to center the ball.
Calculating the offset this way works for any size symbol as long as we keep the current ratios.
.ball {
height: 20px;
width: 20px;
border-radius: 50%;
background: #fff;
position: absolute;
top: 50%;
left: 0;
margin-top: -10px;
z-index: 10;
transform: rotate(180deg);
transform-origin: 58px 10px;
}
Now that we’re all nice and centered, let’s set up our animation again. To center the ball
for the start of the right circle, let’s flip the rotation to -180deg and nudge the transform-origin
x-offset to -38px. Then we’ll modify both circles’ ending values to match up with the new starting
positions. That’ll get the ball moving continuously through the center of our symbol.
@keyframes infinity {
0% {
left: 0;
transform: rotate(180deg);
transform-origin: 58px 10px;
}
50% {
left: 0;
transform: rotate(-180deg);
transform-origin: 58px 10px;
}
50.1% {
left: 192px;
transform: rotate(-180deg);
transform-origin: -38px 10px;
}
100% {
left: 192px;
transform: rotate(180deg);
transform-origin: -38px 10px;
}
}
Staying on Track
With the two circles connected, things are looking pretty smooth, but not really following that infinity symbol all too well. A few tweaks and this ball will be on track.
When we started this animation we had our left rotation with a transform-origin
x-offset of 50px and our right rotation with a transform-origin
x-offset of -30px. So to get
things where they need to be, we’ll animate our transform-origin
between our current and
original values when the ball is crossing that center section. To get a starting point for the
percentage of time needed for this transition we can break the animation down into segments.
We have two halves, each with four corners and each corner has two segments. This gives us 16
segments in total, four are part of our cross section. 100% divided by 16 gives us 6.25%, so I started out with 6% per cross segment
for this transition. After fiddling around with the timing of these transitions a bit (I decided that 4% looked best),
we have our lovely loader animation working just like we'd envisioned.
@keyframes infinityFinal {
0% {
transform: rotate(180deg);
transform-origin: 58px 10px;
left: 0;
}
4% {
transform-origin: 50px 10px;
}
46% {
transform-origin: 50px 10px;
}
50% {
transform: rotate(-180deg);
transform-origin: 58px 10px;
left: 0;
}
50.1% {
left: 192px;
transform: rotate(-180deg);
transform-origin: -38px 10px;
}
54% {
transform-origin: -30px 10px;
}
96% {
transform-origin: -30px 10px;
}
100% {
left: 192px;
transform: rotate(180deg);
transform-origin: -38px 10px;
}
}
The End
Wait... it’s an infinity symbol, there is no end. Anyway, we hope you enjoyed the post, and if you’d like to play around with this on your own you can view the final version on CodePen.