When dealing with complex CSS animations, there is a tendency to create expansive @keyframes
with lots of declarations. There are a couple of tricks though that I want to talk about that might help make things easier, while staying in vanilla CSS:
- Multiple animations
- Timing functions
The first one is more widely used and familiar but the second one is less common. There could be good reasons for that — chaining animations with commas is relatively easier than grokking the various timing functions that are available to us and what they do. There's one especially neat timing function that gives us total control to create custom timing functions. That would be cubic-bezier()
and in this post I will show you the power of it and how it can be used to create fancy animation without too much complexity.
Let's start with a basic example showing how we can move a ball around in interesting directions, like an infinity (∞) shape:
As you can see, there is no complex code — only two keyframes and a "strange" cubic-bezier()
function. And yet, a pretty complex-looking final infinity-shape animation is what we get.
Cool, right? Let's dig into this!
The cubic-bezier() function
Let's start with the official definition:
A cubic Bézier easing function is a type of easing function defined by four real numbers that specify the two control points, P1 and P2, of a cubic Bézier curve whose end points P0 and P3 are fixed at (0, 0) and (1, 1) respectively. The x coordinates of P1 and P2 are restricted to the range [0, 1].
The above curve defines how the output (y-axis) will behave based on the time (x-axis). Each axis has a range of (or ). If we have an animation that lasts two-second ( ), then:
If we want to animate left from 5px
to 20px
, then:
X, the time, is always restricted to ; however, Y, the output, can go beyond .
My goal is to adjust P1 and P2 in order to create the following curves:
You may think this is impossible to achieve because, as stated in the definition, and are fixed at and meaning they cannot be on the same axis. That's true, and we will use some math tricks to "approximate" them.
Parabolic curve
Let's start with the following definition: cubic-bezier(0,1.5,1,1.5)
. That gives us the following curve:
Our goal is to move and make it at which isn’t technically possible. So we will try to fake it.
We previously said that our range is (or ) so let's imagine the case when is very close to . If, for example, we want to animate top from ( ) to ( ) then we can say that both the initial and final states are equal.
Hm, but our element will not move at all, right?
Well, it will move a little because the Y value exceeds ( ). But that's not enough to give us perceptible movement:
Let's update the curve and use cubic-bezier(0,4,1,4)
instead. Notice how our curve is way taller than before:
Curve | Output |
---|---|
But yet, still no movement — even if the top value is crossing
(or
). Let's try cubic-bezier(0,20,1,20)
:
Curve | Output |
---|---|
Yes! it started to move a little. Did you notice the evolution of the curve each time we increase the value? It's making our point "visually" closer to when we zoom out to see the full curve and this is the trick.
By using cubic-bezier(0,V,1,V)
where
is some very big value and both the initial and final states are very close together (or almost equal), we can simulate the parabolic curve.
An example is worth a thousand words:
I applied the "magic" cubic-bezier function in there to the top animation, plus a linear one applied to left. This gives us the curve we want.
Digging into the math
For those of you math-minded folks out there, we can break that explanation down further. A cubic bezier can be defined using the following formula:
Each point is defined as follows:
This gives us the two functions for x and y coordinates:
is our big value and
is within the range
. If we consider our previous example,
will give us the value of top
while
is the time progress. The points
will then define our curve.
Let's find the maximum value of . For this, we need to find the value of that will give us (when the derivative is equal to ):
is a quadratic equation. I will skip the boring part and will give you the result, which is
When is a large value, will be equal to . So, and will be equal to . That means we reach the maximum value at the halfway point in the animation, which conforms to the parabolic curve we want.
Also, will give us and this will allow us to find the max value based on . And since we will always use a big value for , we can simplify to .
We used in the last example, so the max value there would come out to (or ) and we get the following:
- Initial state (
):
top: 200px
- Final state (
):
top: 199.5px
There's a difference of
between
and
. Let's call it the increment. For
(or
) we have an equation of
. Our animated element is reaching top: 12.5px
(
) and gives us the following animation:
top: 200px (at 0% of the time ) → top: 12.5px (at 50% of the time) → top: 199.5px (at 100% of the time)
Or, expressed another way:
top: 200px (at 0%) → top: 12.5px (at 50%) → top: 200px (at 100%)
Let's do the opposite logic. What value of
should we use to make our element reach top: 0px
? The animation will be 200px → 0px → 199.5px
, so we need
to reach
. Our increment is always equal to
. The max value will be equal to
, so
which means
.
Our element is touching the top!
Here is a figure that sums up that math we just did:
Sinusoidal curve
We will use almost the exact same trick to create a sinusoidal curve but with a different formula. This time we will use cubic-bezier(0.5,V,0.5,-V)
Like we did before, let's see how the curve will evolve when we increase the value:
I think you probably get the idea by now. Using a big value for gets us close to a sinusoidal curve.
Here's another one with a continuous animation — a real sinusoidal animation!
The math
Let's get in the math for this one! Folllowing the same formula as before, we will get the following functions:
This time we need to find the minimum and maximum values for . will give us two solutions. After solving for this:
…we get:
For a big value of , we have and . That means that and . That also means that and . In other words, we reach the Max at of the time and Min at of the time.
is equal to and to . We got those values with some rounding considering that is very big.
Our sinusoidal curve should also cross the x-axis (or ) at half the time (or ). In order to prove this, we use the second derivate of — which should be equal to — so .
The solution is , and for a big value, the solution is . That give us and which confirms that our curve crosses the point.
Now let's consider the previous example and try to find the value of
that gets us back to top: 0%
. We have:
- Initial state (
):
top: 50%
- Final state (
):
top: 49.9%
- Increment:
We need
to reach top: 0%
, so
which gives us
.
As you can see, our element is touching the top and disappearing at the bottom because we have the following animation:
top: 50% → top: 0% → top: 50% → top: 100% → top: 50% → and so on ...
A figure to sum up the calculation:
And an example to illustrate all curves together:
Yes, you see four curves! If you look closely, you will notice that I am using two different animations, one going to (an increment of ) and another going to (an increment of ). By changing the sign of the increment, we control the direction of the curve. We can also control the other parameters of the cubic bezier (not the one that should remain a big value) to create more variations from the same curves.
And below, an interactive demo:
Getting back to our example
Let's get back to our initial example of a ball moving around in the shape of an infinity symbol. I simply combined two sinusoidal animations to make it work.
If we combine what we did previously with the concept of multiple animations, we can get astonishing results. Here again is the initial example, this time as an interactive demo. Change the values and see the magic:
Let's go further and add a little CSS Houdini to the mix. We can animate a complex transform declaration thanks to @property
(but CSS Houdini is limited to Chrome and Edge support at the moment).
What kind of drawings can you make with that? Here is a few that I was able to make:
And here is a spirograph animation:
And a version without CSS Houdini:
There's a few things to take away from these examples:
- Each keyframe is defined using only one declaration that contain the increment.
- The position of the element and the animation are independent. We can easily place the element anywhere without the need to adjust the animation.
- We made no calculations. There isn't a ton of angles or pixel values. We only need a tiny value within the keyframe and a big value within the
cubic-bezier()
function. - The whole animation can be controlled just by adjusting the duration value.
What about transition?
The same technique can also be used with the CSS transition property since it follows the same logic when it comes to timing functions. This is great because we're able to avoid keyframes when creating some complex hover effect.
Here's what I made without keyframes. If you were following me you will remember that they are a part of my underline/overlay animation collection 😉
Mario is jumping thanks to the parabolic curve. We needed no keyframes at all to create that shake animation on hover. The sinusoidal curve is perfectly capable of doing all the work.
Here is another version of Mario, this time using CSS Houdini. And, yep, he's still jumping thanks to the parabolic curve:
For good measure, here are more fancy hover effects without keyframes (again, Chrome and Edge only). Spoiler for my next collection 😜
That's it!
Now you have some magic cubic-bezier()
curves and the math behind them. The benefit, of course, is that custom timing functions like this let us do fancy animations without the complex keyframes we generally reach for.
I understand that not everyone is math-minded and that’s okay. There are tools to help, like Matthew Lein's Ceaser, which lets you drag the curve points around to get what you need. And, if you don't already have it bookmarked, cubic-bezier.com is another one. If you want to play with cubic-bezier outside the CSS world, I recommend desmos where you can see some math formulas.
Regardless of how you get your cubic-bezier()
values, hopefully now you have a sense of their powers and how they can help make for nicer code in the process.
Top comments (17)
Lol to all those people who ones assumed, front end don't require any math.
Front end require math and geometry ;) (like I am doing here: dev.to/afif/fully-responsive-css-f... and here: dev.to/afif/responsive-hexagon-gri...) and more Math-related posts are coming ;)
Looking forward to it
State of the art work.
What a deep-dive, awesome article! Thank you for that one!
If I may add a suggestion: to make the maths part a bit more readable, the editor allows for KaTeX using liquid tags, so you can format the formulas in well-known notations, such as fractions, subscripts, superscripts, sum signs (capital sigma), etc.
thanks for the tip, now the article looks a lot better 👍
Love it, thank you again for the great article 😀
Oh we have math here, thanks god! will edit right now ... it wasn't the case on CSS-tricks 😔
Are you aware of 3Blue1Brown?
One of my fav YouTubers on awesome topics like these 👌💯
not really, my engineer days are behind me now but might be good to refresh my memory 😉
Also the opensource animation engine he uses manim 🔥😉
Another great article!
Just commenting for the algorithm 😜
wow, who says that advanced CSS tricks are not rocket science.
thank you for the deep dive
Gustavo.
Great Article bro
Nice math :D .
haha usually I use cubic-bezier.com because it takes much less time.
Yes, I also know desmos.. I use it for my assignments ;)
Awesome very nice
This article makes me feel I don't know any CSS at all! It's scary :))