Bézier Curves for Web Animations in JavaScript
Introduction
Every time you see a smooth, natural-looking animation on the web, whether it is a button that gently eases into place, a menu that slides open with a subtle bounce, or a modal that decelerates before stopping, Bézier curves are doing the heavy lifting behind the scenes.
Bézier curves are the mathematical backbone of CSS transitions, CSS animations, and JavaScript-driven animation timing. They define how an animation progresses over time, not just where it starts and ends, but the character and feel of every frame in between.
Understanding Bézier curves gives you full creative control over your animations. Instead of relying solely on predefined keywords like ease or linear, you will be able to craft custom timing functions that make your interfaces feel alive.
In this guide, you will learn what Bézier curves are, how control points shape motion, how to use the CSS cubic-bezier() function, and how to recognize and apply the most common easing patterns in your projects.
What Is a Bézier Curve?
A Bézier curve is a parametric curve defined by a set of control points. In web development, we work almost exclusively with cubic Bézier curves, which are defined by exactly four control points: P0, P1, P2, and P3.
The curve starts at P0 and ends at P3. The two intermediate points, P1 and P2, do not sit on the curve itself. Instead, they act like magnets that pull the curve toward them, shaping its trajectory.
The Coordinate System for Animations
When Bézier curves are used for animation timing, the coordinate system has a specific meaning:
- X-axis represents time (from
0to1, where0is the start and1is the end of the animation) - Y-axis represents animation progress (from
0to1, where0is the initial state and1is the final state)
The first point P0 is always fixed at (0, 0) (animation start), and the last point P3 is always fixed at (1, 1) (animation end). You only define the two middle control points P1 and P2.
Y (progress)
1 | * P3 (1, 1)
| * *
| *
| * <- The curve traces the animation's progress
| *
| *
| *
0 *________________________
P0 (0, 0) 1 X (time)
This is the concept behind every CSS cubic-bezier() function: you provide the coordinates of P1 and P2, and the browser calculates the curve that determines how your animation behaves at every moment.
The mathematical formula for a cubic Bézier curve is:
B(t) = (1-t)³·P0 + 3(1-t)²·t·P1 + 3(1-t)·t²·P2 + t³·P3
where t goes from 0 to 1. You do not need to memorize this formula. What matters is understanding how the control points influence the shape.
Understanding Cubic Bézier Curves Visually
The best way to understand Bézier curves is to see them in action. Let us walk through several scenarios to build your intuition.
The Simplest Case: A Straight Line
When both control points are evenly distributed along the diagonal, you get a perfectly straight line:
P1 = (0.0, 0.0)
P2 = (1.0, 1.0)
cubic-bezier(0.0, 0.0, 1.0, 1.0)
Y (progress)
1 | *
| *
| *
| *
| * <- Perfectly linear: constant speed
| *
| *
| *
0 *________________________
0 1 X (time)
This is the linear timing function. The animation progresses at a constant rate. At 50% of the time, the animation is at 50% progress. No acceleration, no deceleration.
A Curve That Starts Slow and Ends Fast
Now imagine pulling P1 down and to the right:
P1 = (0.42, 0.0)
P2 = (1.0, 1.0)
cubic-bezier(0.42, 0.0, 1.0, 1.0)
Y (progress)
1 | *
| *
| *
| *
| *
| * <- Starts slow, then accelerates
| *
| *
0 **_______________________
0 1 X (time)
The curve stays close to the bottom at first (slow progress), then rises steeply (fast progress). This is called ease-in: the animation "eases into" its motion gradually.
A Curve That Starts Fast and Ends Slow
If we do the opposite, pulling P2 up and to the left:
P1 = (0.0, 0.0)
P2 = (0.58, 1.0)
cubic-bezier(0.0, 0.0, 0.58, 1.0)
Y (progress)
1 | * * * * * *
| *
| *
| *
| * <- Starts fast, then decelerates
| *
| *
|*
0 *________________________
0 1 X (time)
The animation rushes forward at the beginning and gradually slows to a stop. This is ease-out: the animation "eases out" of its motion.
Control Points and How They Affect Motion
The four values in cubic-bezier(x1, y1, x2, y2) are the coordinates of the two control points P1(x1, y1) and P2(x2, y2). Understanding what each coordinate does is the key to mastering custom animations.
The X Coordinates Control "When"
The x values of the control points determine when the acceleration or deceleration happens along the timeline.
- A small x1 (like
0.0) meansP1pulls the curve from the very beginning of the timeline - A large x1 (like
0.8) means the initial phase of the curve is drawn out before acceleration kicks in - Similarly, x2 controls the timing of the second half of the curve
// P1 pulls early in the timeline
cubic-bezier(0.1, 0.7, 0.9, 0.3)
// P1 pulls late in the timeline
cubic-bezier(0.8, 0.7, 0.9, 0.3)
The x values must always be between 0 and 1 because they represent time, and time cannot go backward or exceed the animation duration. The browser will clamp or reject values outside this range.
The Y Coordinates Control "How Much"
The y values determine the intensity of the effect, meaning how far the animation progress is pulled.
y1 = 0means the animation starts with no progress (slow start)y1 = 1means the animation jumps to significant progress immediately (fast start)
Here is where things get interesting: y values CAN go below 0 or above 1. This creates overshoot effects.
Overshoot: Y Values Beyond the 0-1 Range
When a y value exceeds 1, the animation temporarily goes past its final state before settling back. When a y value goes below 0, the animation moves backward before going forward.
/* Overshoot at the end (bouncy feel) */
.bounce-end {
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Overshoot at the start (pull-back effect) */
.pull-back {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1);
}
Let us visualize the overshoot curve:
Y (progress)
1.5| *
| * *
1 | * * * * * * *
| *
| * <- Y goes above 1: overshoot!
| *
| *
0 *________________________
0 1 X (time)
This effect is what makes animations feel physical and natural. A ball thrown into a basket might overshoot slightly and bounce. A drawer sliding open might go a tiny bit too far before settling. These are the kinds of details that Bézier curves let you express.
A Practical Example: Comparing Control Point Effects
Let us see how different control points change the same animation:
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 80px;
height: 80px;
margin: 10px;
background: #3498db;
border-radius: 8px;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
text-align: center;
}
.container:hover .box {
transform: translateX(300px);
}
.linear {
transition: transform 1s cubic-bezier(0, 0, 1, 1);
}
.ease-in {
transition: transform 1s cubic-bezier(0.42, 0, 1, 1);
}
.ease-out {
transition: transform 1s cubic-bezier(0, 0, 0.58, 1);
}
.bounce {
transition: transform 1s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>
</head>
<body>
<div class="container">
<div class="box linear">Linear</div>
<div class="box ease-in">Ease In</div>
<div class="box ease-out">Ease Out</div>
<div class="box bounce">Bounce</div>
<p>Hover over this area to animate</p>
</div>
</body>
</html>
When you hover, all four boxes move 300px to the right over 1 second, but each one feels completely different because of the Bézier curve controlling its timing.
The CSS cubic-bezier() Function
The cubic-bezier() function is the standard way to define custom timing functions in CSS transitions and animations.
Syntax
transition-timing-function: cubic-bezier(x1, y1, x2, y2);
animation-timing-function: cubic-bezier(x1, y1, x2, y2);
Where:
x1, y1are the coordinates of control pointP1x2, y2are the coordinates of control pointP2x1andx2must be in the range[0, 1]y1andy2can be any number (including negative values and values greater than 1)
Using cubic-bezier() in Transitions
.button {
background-color: #3498db;
transform: scale(1);
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
background-color 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.button:hover {
background-color: #2980b9;
transform: scale(1.05);
}
Using cubic-bezier() in Keyframe Animations
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
Using cubic-bezier() from JavaScript
You can set timing functions dynamically with JavaScript:
const element = document.querySelector('.animated-box');
// Setting via style property
element.style.transition = 'transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)';
// Or using the Web Animations API
element.animate(
[
{ transform: 'translateX(0)' },
{ transform: 'translateX(300px)' }
],
{
duration: 500,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
fill: 'forwards'
}
);
The Web Animations API (WAAPI) accepts the same cubic-bezier() string in its easing option, making it easy to use the same timing functions across CSS and JavaScript animations.
Building a Custom Bézier Curve Interactively
One of the best ways to craft your own curves is to use browser DevTools or online tools:
- Chrome DevTools: Click on the timing function value in the Styles panel to open a visual Bézier curve editor
- cubic-bezier.com: A dedicated tool for creating and previewing curves
- easings.net: A visual catalog of common easing functions with their
cubic-bezier()values
// A function to test different curves programmatically
function animateWithCurve(element, curve, duration = 1000) {
element.style.transition = `transform ${duration}ms cubic-bezier(${curve})`;
element.style.transform = 'translateX(300px)';
// Reset after animation completes
setTimeout(() => {
element.style.transition = 'none';
element.style.transform = 'translateX(0)';
}, duration + 100);
}
// Usage
const box = document.querySelector('.box');
animateWithCurve(box, '0.25, 0.1, 0.25, 1'); // ease
animateWithCurve(box, '0.42, 0, 0.58, 1'); // ease-in-out
animateWithCurve(box, '0.34, 1.56, 0.64, 1'); // overshoot
Common Easing Functions Explained
CSS provides several predefined timing function keywords that map to specific cubic-bezier() values. Let us explore each one, understand its curve shape, and learn when to use it.
linear
/* Equivalent to: */
cubic-bezier(0, 0, 1, 1)
Y
1 | *
| *
| *
| *
| * Constant speed, no acceleration
| *
| *
0 *____________________
0 1 X
Behavior: Constant speed from start to finish. No acceleration, no deceleration.
When to use:
- Color transitions (where constant change looks natural)
- Progress bars
- Opacity fades
- Situations where you want mechanical, predictable motion
When to avoid:
- Moving physical objects (constant speed looks robotic and unnatural)
.progress-bar {
transition: width 0.3s linear;
}
ease
/* Equivalent to: */
cubic-bezier(0.25, 0.1, 0.25, 1)
Y
1 | * * * *
| *
| *
| *
| * Fast start, gradual deceleration
| *
| *
0 **___________________
0 1 X
Behavior: Starts slightly slowly, quickly accelerates, then decelerates gently toward the end. This is the default timing function when none is specified.
When to use:
- General-purpose animations
- When you do not want to think too hard about timing
- UI element state changes
.card {
/* 'ease' is the default, but being explicit is good practice */
transition: box-shadow 0.3s ease;
}
ease-in
/* Equivalent to: */
cubic-bezier(0.42, 0, 1, 1)
Y
1 | **
| *
| *
| *
| * Slow start, fast finish
| *
| *
0 ***__________________
0 1 X
Behavior: Starts slowly and accelerates toward the end. The animation "eases in" to full speed.
When to use:
- Elements exiting the viewport (they need to pick up speed as they leave)
- Objects that are gaining momentum
When to avoid:
- Elements appearing on screen (the slow start feels sluggish and unresponsive)
.notification-exit {
transition: transform 0.4s ease-in, opacity 0.4s ease-in;
}
.notification-exit.hidden {
transform: translateY(-100%);
opacity: 0;
}
ease-out
/* Equivalent to: */
cubic-bezier(0, 0, 0.58, 1)
Y
1 | * * * * * *
| *
| *
| *
| * Fast start, slow finish
| *
| *
0 *____________________
0 1 X
Behavior: Starts at full speed and decelerates to a stop. The animation "eases out" of its motion.
When to use:
- Elements entering the viewport (they should appear quickly and settle in)
- Dropdown menus opening
- Modals appearing
- Any element that needs to feel responsive
.modal-enter {
animation: fadeSlideIn 0.3s ease-out forwards;
}
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Use ease-out for elements entering the screen and ease-in for elements leaving the screen. This matches how physical objects behave: they start fast (something triggered them) and slow down due to friction.
ease-in-out
/* Equivalent to: */
cubic-bezier(0.42, 0, 0.58, 1)
Y
1 | * * * *
| *
| *
| *
| * Slow start, slow finish, fast middle
| *
| *
0 ** * ________________
0 1 X
Behavior: Starts slowly, accelerates in the middle, and decelerates at the end. A symmetric curve.
When to use:
- Elements that both start and stop within the viewport
- Toggling between two states (accordion open/close)
- Carousel slides
- Any animation where both the beginning and end should feel smooth
.accordion-content {
transition: max-height 0.4s ease-in-out;
overflow: hidden;
}
Summary Table of Predefined Easing Functions
| Keyword | cubic-bezier() | Start | End | Best For |
|---|---|---|---|---|
linear | (0, 0, 1, 1) | Constant | Constant | Colors, opacity, progress bars |
ease | (0.25, 0.1, 0.25, 1) | Slow | Slow | Default, general purpose |
ease-in | (0.42, 0, 1, 1) | Slow | Fast | Elements exiting |
ease-out | (0, 0, 0.58, 1) | Fast | Slow | Elements entering |
ease-in-out | (0.42, 0, 0.58, 1) | Slow | Slow | State toggles, in-place animations |
Advanced Easing Patterns
Beyond the five predefined keywords, there are several popular custom curves used extensively in production interfaces.
Ease-Out-Back (Overshoot)
This curve goes slightly past the target and then settles back. It creates a satisfying, springy feel.
.ease-out-back {
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
Y
1.5| *
| * *
1 | * * * * *
| *
| * Overshoots past 1, then settles
| *
| *
0 *____________________
0 1 X
Use case: Popup modals, tooltips appearing, notification badges.
.tooltip {
opacity: 0;
transform: scale(0.8) translateY(10px);
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.trigger:hover .tooltip {
opacity: 1;
transform: scale(1) translateY(0);
}
Ease-In-Back (Pull-Back Before Moving)
The animation briefly moves backward before accelerating forward, like pulling back a slingshot.
.ease-in-back {
transition: transform 0.5s cubic-bezier(0.6, -0.28, 0.735, 0.045);
}
Y
1 | *
| *
| *
| *
| *
| *
0 |*__*_______________
| * Goes below 0 before moving forward
-0.2
0 1 X
Use case: Elements exiting with a dramatic flair, objects being "thrown."
Ease-In-Out-Back (Overshoot on Both Ends)
Combines both effects for a very dynamic, energetic feel.
.ease-in-out-back {
transition: transform 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
Use case: Playful UI elements, toggle switches, game-like interfaces.
Sharp/Snappy Ease-Out
A more aggressive ease-out that makes the animation feel very responsive and immediate.
.snappy {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
This curve is often called "expo out" or "quint out" in animation libraries. It feels faster and more decisive than the standard ease-out.
Use case: Micro-interactions, button presses, quick state changes.
// Using the snappy curve with Web Animations API
document.querySelector('.dropdown').animate(
[
{ opacity: 0, transform: 'scaleY(0.95) translateY(-8px)' },
{ opacity: 1, transform: 'scaleY(1) translateY(0)' }
],
{
duration: 200,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
fill: 'forwards'
}
);
Implementing Bézier Curves in JavaScript
While CSS handles Bézier curves natively, you might need to calculate Bézier values in JavaScript for custom animation loops using requestAnimationFrame.
A Simple Cubic Bézier Function
Here is a basic implementation that computes the Y value (progress) for a given T value (time fraction):
function cubicBezier(t, p1x, p1y, p2x, p2y) {
// For animation timing, we need to:
// 1. Find the T parameter that corresponds to our time X
// 2. Calculate the Y value at that T
// Simplified: using Newton's method to find T for a given X
let tGuess = t;
for (let i = 0; i < 8; i++) {
const currentX = bezierPoint(tGuess, p1x, p2x);
const slope = bezierSlope(tGuess, p1x, p2x);
if (Math.abs(slope) < 1e-6) break;
tGuess -= (currentX - t) / slope;
}
return bezierPoint(tGuess, p1y, p2y);
}
function bezierPoint(t, p1, p2) {
// B(t) = 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³
// (P0 = 0, P3 = 1 for animation curves)
return 3 * (1 - t) * (1 - t) * t * p1 +
3 * (1 - t) * t * t * p2 +
t * t * t;
}
function bezierSlope(t, p1, p2) {
return 3 * (1 - t) * (1 - t) * p1 +
6 * (1 - t) * t * (p2 - p1) +
3 * t * t * (1 - p2);
}
Using It in a requestAnimationFrame Loop
function animate(element, from, to, duration, bezierParams) {
const [p1x, p1y, p2x, p2y] = bezierParams;
const startTime = performance.now();
function frame(currentTime) {
let elapsed = (currentTime - startTime) / duration;
elapsed = Math.min(elapsed, 1); // Clamp to 1
// Get the eased progress using our Bézier function
const progress = cubicBezier(elapsed, p1x, p1y, p2x, p2y);
// Interpolate between 'from' and 'to'
const currentValue = from + (to - from) * progress;
element.style.transform = `translateX(${currentValue}px)`;
if (elapsed < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
// Usage: animate a box 300px to the right with ease-out timing
const box = document.querySelector('.box');
animate(box, 0, 300, 600, [0, 0, 0.58, 1]);
Implementing Bézier calculations yourself is educational, but for production code, prefer CSS transitions, the Web Animations API, or a well-tested animation library. Browser-native implementations are GPU-accelerated and far more performant than manual JavaScript calculations for simple property animations.
Choosing the Right Easing for Your Animation
Selecting the appropriate Bézier curve depends on the context of the animation. Here is a practical decision guide:
Entering the Screen
Elements appearing for the first time should feel responsive. Use curves that start fast and end slow.
/* Recommended: ease-out or snappy ease-out */
.enter {
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
Leaving the Screen
Elements disappearing should accelerate away. Use curves that start slow and end fast.
/* Recommended: ease-in */
.exit {
animation-timing-function: cubic-bezier(0.42, 0, 1, 1);
}
Moving Within the Screen
Elements that start and stop within the viewport should feel smooth at both ends.
/* Recommended: ease-in-out */
.move {
animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
}
Playful or Attention-Grabbing Interactions
For elements that need to feel bouncy or energetic, use curves with overshoot.
/* Recommended: ease-out-back */
.playful {
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}
Continuous or Looping Animations
For animations that repeat indefinitely (spinners, loading indicators), use linear to maintain constant speed.
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Common Mistakes and Best Practices
Mistake: Using linear for Movement
/* Feels robotic and unnatural */
.bad {
transition: transform 0.3s linear;
}
/* Feels natural and responsive */
.good {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
Physical objects in the real world never move at constant speed. They accelerate and decelerate. Using linear for spatial movement almost always looks wrong.
Mistake: Using ease-in for Appearing Elements
/* Feels sluggish - slow start when user expects immediate feedback */
.bad-enter {
animation: appear 0.3s ease-in forwards;
}
/* Feels responsive - fast start, gentle landing */
.good-enter {
animation: appear 0.3s ease-out forwards;
}
When a user clicks a button and a modal appears, the slow start of ease-in creates a feeling of lag. Use ease-out so the element appears quickly and settles smoothly.
Mistake: Excessive Overshoot
/* Too much - feels broken, not bouncy */
.too-much {
transition: transform 0.5s cubic-bezier(0.2, 4, 0.3, 1);
}
/* Just right - subtle and polished */
.just-right {
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
A Y value of 4 means the animation overshoots to 400% of its final value before coming back. This looks like a glitch, not a design choice. Keep overshoot subtle, typically with Y values between 1.2 and 1.8.
When in doubt, start with cubic-bezier(0.16, 1, 0.3, 1) for entering elements. It is the most universally flattering easing curve: snappy, responsive, and polished. Major design systems like Material Design and Apple's Human Interface Guidelines use similar curves.
Quick Reference: Popular Bézier Curves
Here is a collection of battle-tested curves you can copy directly into your projects:
:root {
/* Standard easings */
--ease-in: cubic-bezier(0.42, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.58, 1);
--ease-in-out: cubic-bezier(0.42, 0, 0.58, 1);
/* Snappy / responsive */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-expo: cubic-bezier(0.7, 0, 0.84, 0);
/* Overshoot / bouncy */
--ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-in-back: cubic-bezier(0.6, -0.28, 0.735, 0.045);
--ease-in-out-back: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Material Design */
--md-standard: cubic-bezier(0.4, 0, 0.2, 1);
--md-decelerate: cubic-bezier(0, 0, 0.2, 1);
--md-accelerate: cubic-bezier(0.4, 0, 1, 1);
}
// Same curves available in JavaScript
const EASINGS = {
easeOut: 'cubic-bezier(0, 0, 0.58, 1)',
easeOutExpo: 'cubic-bezier(0.16, 1, 0.3, 1)',
easeOutBack: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
easeInOut: 'cubic-bezier(0.42, 0, 0.58, 1)',
};
// Usage with Web Animations API
element.animate(keyframes, {
duration: 300,
easing: EASINGS.easeOutExpo,
fill: 'forwards'
});
Summary
Bézier curves are the foundation of smooth, natural-feeling web animations. Here are the key takeaways:
- A cubic Bézier curve is defined by four points, but only
P1andP2are customizable (sinceP0andP3are fixed at start and end) - The X coordinates of control points must stay between 0 and 1 (they represent time), while Y coordinates can go beyond that range to create overshoot effects
- CSS provides five keywords (
linear,ease,ease-in,ease-out,ease-in-out) that map to specificcubic-bezier()values - Use ease-out for elements entering, ease-in for elements leaving, and ease-in-out for in-place transitions
- Y values beyond the 0-1 range create overshoot, which adds a bouncy, physical quality to animations
- For JavaScript animations with
requestAnimationFrame, you can implement Bézier math manually, but prefer CSS transitions or the Web Animations API whenever possible - Tools like Chrome DevTools, cubic-bezier.com, and easings.net make it easy to experiment and find the perfect curve
Mastering Bézier curves transforms your animations from functional to delightful. Start with the predefined keywords, experiment with the visual tools, and gradually build a library of custom curves that define the personality of your interface.