Over the past few months, I've been working a lot on my Design System and one aspect of this work that I enjoyed focusing on is micro-interactions ✨. These can be very tedious to get right, but if built properly they can make components go from good to exceptional!
However, more recently, I brought my attention to something a bit more subtle. While iterating on a button component hover transition, using timing functions such as linear
ease-in
or ease-out
did not feel quite right. The only way I achieved a satisfying result was to set my CSS transition property to the following: cubic-bezier(0.34, 1.56, 0.64, 1)
, which I copied-pasted from a Codepen without really knowing what those values and function were doing, which, to be honest with you, is the kind of thing that always bothers me 😅. I like to understand the tools I'm using.
So, I went down a rabbit hole of math, animations, and code to have a clear understanding of what cubic-bezier
really is, and also what the numbers I passed to that function meant and how they translate to motion. Hence the title of this blog post! We'll first deep dive into the math behind cubic-bezier
, then try to visualize how the graphical representation of this function translates into motion and how it relates to other timing functions you might be familiar with. All of that, illustrated through interactive visualizations to allow you to see and tweak the math that's behind these beautiful transitions ⭐️.
The math behind Bézier curves
First, what's really behind this cubic-bezier
function we keep seeing in our CSS codebases? Well, to simply put it, this function defines what is called a Cubic Bézier curve. It's a specific type of curve, that helps represent how a transition goes from an initial state to a final state.
Why Cubic? That is where the math part of this article comes in. To start let's look at the definition of the umbrella term "Bézier curve":
A Bézier curve is a parametric curve defined by a set of control points
We can start our discovery of Bézier curves by looking at their simplest form to understand what these "control points" are, and then slowly make our way up in complexity to reach its cubic form.
Linear Interpolation
Let's consider two distinct points P0
and P1
, and another point P
that's located between them. In this scenario, P0
and P1
are the control points of the curve, and P is a point that moves between them. We can define the position of P
with a value between 0
and 1
named t
that is similar to a percentage:
- if
t = 1
,P
will move toP1
- if
t = 0
,P
will move toP0
- any values between 0 and 1 would be a "mix" of
P0
andP1
I represented this example in the widget below, where P0
and P1
are at the extremities of the curve, and P
is the blue dot moving between them. You'll see that the closer from 1 t
is, the closer
from the end of the curve P
will be.
This is called a Linear Interpolation.
Quadratic Bézier
Let's add another point! We can now have two interpolated points, between each segment, moving respectively on the axis P0 -> P1
and P1 -> P2
. If we link these two points (the red dots) with a segment and position an interpolated point (the blue dot) on it as well, we'll obtain something rather interesting:
You can see that the blue dot follows a specific path that resembles a curve. This specifc one is called a Quadratic Bézier curve.
Here's the Javascript version of that formula that I use to get the coordinates x
and y
of all the positions of the blue dot for 1 second at 60 frames per second to draw the curve above:
1const quadratic = (P0, P1, P2) => {2const x0 = P0.x;3const y0 = P0.y;45const x1 = P1.x;6const y1 = P1.y;78const x2 = P2.x;9const y2 = P2.y;1011const x = (t) =>12Math.pow(1 - t, 2) * x0 + 2 * (1 - t) * t * x1 + Math.pow(t, 2) * x2;1314const y = (t) =>15Math.pow(1 - t, 2) * y0 + 2 * (1 - t) * t * y1 + Math.pow(t, 2) * y2;1617const res = [];1819// Get all the points for a transition at 60 frames per second that lasts 1s20for (let t = 0; t <= 1; t = t + 1 / 60) {21const valX = x(t);22const valY = y(t);23res.push({ x: valX, y: valY });24}25res.push({ x: 1, y: 0 });2627return res;28};
Cubic Bézier
Now, if we add a fourth point (so we now have the control points P0
, P1
, P2
, and P3
), and follow the same process as before:
- we add an interpolated point between each of the segments that link the 4 points (in red below)
- we link these interpolated points and define an interpolated point for each of the newly obtained segments (in green)
- we link again these points, draw a segment between them, and add yet another interpolated point (in blue)
we finally obtain a the formula representing a Cubic Bézier curve. I know this may sound very complicated at this point, so I hope the visualization below will do a good job at illustrating how this curve is obtained:
Below you'll find the JS version of that formula which, like its quadratic counterpart, will return all the coordinates x
and y
of all the points describing the position of the blue dot along this Cubic Bézier curve, for 1 second at 60 frames per second:
1const cubic = (P0, P1, P2, P3) => {2const x0 = P0.x;3const y0 = P0.y;45const x1 = P1.x;6const y1 = P1.y;78const x2 = P2.x;9const y2 = P2.y;1011const x3 = P3.x;12const y3 = P3.y;1314const y = (t) =>15Math.pow(1 - t, 3) * y0 +163 * Math.pow(1 - t, 2) * t * y1 +173 * (1 - t) * Math.pow(t, 2) * y2 +18Math.pow(t, 3) * y3;1920const x = (t) =>21Math.pow(1 - t, 3) * x0 +223 * Math.pow(1 - t, 2) * t * x1 +233 * (1 - t) * Math.pow(t, 2) * x2 +24Math.pow(t, 3) * x3;2526const res = [];2728for (let t = 0; t <= 1; t = t + 1 / 60) {29const valX = x(t);30const valY = y(t);31res.push({ x: valX, y: valY });32}33res.push({ x: 1, y: 0 });3435return res;36};
Visualizing the motion
We just did the hard part! 🎉 We broke down the math behind Bézier curves into small bits and slowly combined them to obtain the Cubic Bézier formula and represent its corresponding curve. Now we can see how this Cubic Bézier curve relates to transition and motion in general.
For this part, we consider the Cubic Bézier formula from the previous section and draw its representation but with a twist:
- we set the control point
P0
with the coordinatesx:0, y:0
- we set the control point
P3
with the coordinatesx:1, y:1
The reason behind that is that the cubic-bezier
function in CSS uses two implicit points:
P0
represents the initial timex:0
and the initial statey:0
. It's the point where our curve starts.P3
represents the final timex:1
and the final statey:1
. It's the point where our curve ends.
Thus, this leaves us with only two control points to define: P1
and P2
. Now, remember when I gave the example of a cubic-bezier
function I used for one of my transition in the intro?
cubic-bezier(0.34, 1.56, 0.64, 1)
The four numbers passed to this function are the coordinates of the control points P1
and P2
: cubic-bezier(P1.x, P1.y, P2.x, P2.y)
. Setting those points gives us a specific curve representing the motion that the element with this timing function will follow during its transition.
To better illustrate that, I built the little Cubic Bezier visualizer below ✨. With it, you can change the position of P1
and P2
by moving the gray handles and get the Cubic Bézier curve corresponding to those values!
The visualizer also allows you to:
- see the position an element (the blue dot in this case) throughout its motion for each frame
- project the position of the element to observe the change in
y
value, i.e. the trace of the motion of the element through time, by togglingProject Points
on.
By projecting the positions throughout the transition, we can "see" the motion of our element represented by a Cubic Bézier with these specific control points. This is how the "math becomes motion".
Some interesting things you can observe with the motion of this point:
- we render the position of the point at each frame of the motion
- the further apart two consecutive points in the trace are, the faster the motion is: the blue dot spends "less time" at a given position.
- the more narrow the gap between two consecutive points in the trace is, the slower the motion is: the blue dot spends "more time" at that given position.
Easing functions
Now that we know what is truly behind the cubic-bezier
CSS function, you might be wondering how the other timing functions you might be familiar with such as ease-in
or linear
relate to that. In a nutshell, they are actually Cubic Bézier themselves!
Cubic Béziers, Cubic Béziers everywhere
We can describe any of linear
, ease-in
, ease-out
, ease-out
in cubic-bézier
form. The only thing to do to obtain these specific timing functions is to set the values of the coordinates for P1
and P2
accordingly.
These are just the set of cubic-bezier
timing functions available to us out of the box in CSS. There are many types of "ease" transitions that can be represented with specific Cubic Bézier curves. You can visualize some of those below with their corresponding P1
and P2
points:
Thus, not only uncovering the math behind Cubic Bézier helped us understand the cubic-bézier
CSS function, but also a large number of easing functions that are used by many on a day-to-day basis!
Cubic Bézier in Framer Motion
Another aspect that re-affirms the tight relationship between Cubic Bézier and easing functions can be found in the design choices made in Framer Motion's transition
object.
Unlike what we've seen so far with CSS, there's is no cubic-bézier
function per se in Framer Motion. To describe this type of transition you just need to pass the values of the coordinates of your P1
and P2
points as an array to the ease
property:
Example of cubic-bezier like transition in Framer Motion
1import { motion } from 'framer-motion';23const Button = (props) => {4const buttonVariants = {5initial: {6scale: 1,7},8hover: {9scale: 0.94,10},11};1213return (14<motion.button15{...props}16initial="initial"17whileHover="hover"18variants={buttonVariants}19transition={{20ease: [0.34, 1.56, 0.64, 1],21}}22/>23);24};
Conclusion
Wow, what a ride! We went from looking at cubic-bezier(0.34, 1.56, 0.64, 1)
a bit clueless and not knowing what it meant to:
- understand the mathematical concepts that govern Bézier curves
- being able to draw the graphical representation of Cubic Bézier and understand how it translates to motion
- analyze the close relationship between
cubic-bézier
and the easing functions we've always been familiar with
Yet, despite having learned a lot together, we've just scratched the surface! We only took a look at CSS but Bézier curves, and especially its cubic form, can be found in many other frontend adjacent tools/process like:
- Design tools like Figma, to draw anything from curves, shapes, and even fonts!
I hope this blog post satisfied your curiosity and helped you learn some of the cool things that hide behind the tools we use on a day-to-day basis. You can now play with the cubic-bézier
function with confidence in your code and know exactly what to tweak to come up with unique / delightful transitions and animations for your components.
Quick shoutout to 3 awesome people who helped me directly or indirectly to produce this piece by sharing their own creations around this subject:
@pixelbeat who created an awesome Framer prototype to visualize easing curves
@nansdotio who built a super slick CSS transition visualizer
@FreyaHolmer who made an absolutely amazing Youtube video about Bézier curves. She goes way further into the weeds than this article, thus I highly recommend checking this video out if you want to go further. Her way of illustrating and explaining these complex concepts is really inspiring.
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
A deep dive into the math behind Bézier curves, from simple linear interpolations to Cubic Bézier and how they are used to describe motion. This article introduces the concepts underneath cubic-bezier and easing timing functions that are used in CSS and Framer Motion transitions through easy-to-understand interactive examples.