Two animations can have the exact same duration and move between the exact same positions, yet one feels mechanical and cheap while the other feels physical and alive. The difference is almost always the easing curve.
Most developers never give them a second thought. They ship with ease, linear, orease-in-out and call it done. Not because it's a good choice, but because it's the path of least resistance. The result is interfaces that technically animate but never quite feel alive.
In this article we'll go deeper than the defaults. We'll break down how cubic-bezier curves actually work, what gives motion its physical quality, and how far you can push a single timing function to make an interface feel hand-crafted.
1. What Is a cubic-bezier?
A cubic-bezier is a mathematical curve defined by four points: a fixed start at (0, 0), a fixed end at (1, 1), and two movable control pointsP1 and P2. In CSS, you only specify the two control points:
transition-timing-function: cubic-bezier(x1, y1, x2, y2);
/* โโP1โโ โโP2โโ */The horizontal axis represents time (0 = start, 1 = end of the transition). The vertical axis represents progress (0 = initial value, 1 = final value). The curve maps time to progress; the slope at any point on the curve is the speed of the animation at that moment.
Progress over time
Same 1200ms. The curve controls how fast it moves at each moment.
Notice how linear covers equal distance per unit time: predictable, but lifeless. ease starts a bit faster and decelerates. ease-out begins at full speed and coasts to a stop, which feels more natural because it mimics real-world friction.
2. Understanding the Four Values
Each control point has an x and a y coordinate. There's one important constraint: the x values must stay in the [0, 1] range. The y values, however, can go anywhere, and that's where things get interesting.
cubic-bezier(x1, y1, x2, y2)
/*
x1, x2 โ must be in [0, 1] (time constraints)
y1, y2 โ can be anything (progress can overshoot)
P1 (x1, y1) โ influences the start of the curve
P2 (x2, y2) โ influences the end of the curve
*/P1 is connected to the start point (0, 0) with a line, and P2 is connected to the end point (1, 1). These lines are the handles. Pulling a handle away from the diagonal increases the curve's speed in that region. Pushing it toward the diagonal flattens the motion.
The five named CSS easings are just shortcuts for specific cubic-bezier values:
ease โ cubic-bezier(0.25, 0.10, 0.25, 1.00)
ease-in โ cubic-bezier(0.42, 0.00, 1.00, 1.00)
ease-out โ cubic-bezier(0.00, 0.00, 0.58, 1.00)
ease-in-out โ cubic-bezier(0.42, 0.00, 0.58, 1.00)
linear โ cubic-bezier(0.00, 0.00, 1.00, 1.00)Same duration (900ms), different curves. Watch where each ball is at the midpoint.
The most striking comparison here is ease-in vs ease-out. Ease-in starts slow and accelerates, like an object being pushed from rest. For UI elements leaving the screen that can work, but for elements entering, it always feels sluggish. Ease-out is almost always the better default for entering elements.
3. Going Beyond [0, 1]: Overshoot and Anticipate
When a y value exceeds 1.0, the animation briefly surpasses its target before coming back. When a y value drops below 0, the animation briefly goes in the opposite direction before correcting. These are called overshoot and anticipate.
This is where CSS cubic-bezier gets genuinely expressive. A slight overshoot tricks the eye into reading the motion as physical, like a rubber band or a spring. Without any overshoot, even a well-crafted ease can feel like a computer moving something. With a subtle overshoot, it suddenly feels alive.
/* Standard ease-out โ stops exactly at the target */
transition-timing-function: cubic-bezier(0.00, 0.00, 0.58, 1.00);
/* Spring โ overshoots then snaps back */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1.00);
/* ^^^^
y1 above 1.0 = overshoot */
/* Anticipate โ dips before launching */
transition-timing-function: cubic-bezier(0.36, -0.30, 0.63, 1.30);
/* ^^^^^
y1 below 0 = moves in reverse briefly */Clear overshoot: physical and alive.
cubic-bezier(0.34, 1.56, 0.64, 1)Drag the slider upward past 1.0 and watch the ball briefly fly past its destination before snapping back. Around 1.4โ1.6 is the sweet spot for most UI. Above 1.8, it starts feeling like a bounce and draws too much attention to itself.
opacity), the browser clamps the value and the overshoot is invisible. Use overshoot on spatial properties: transform, left, width, scale.4. Using cubic-bezier in Practice
You can use cubic-bezier anywhere CSS accepts an easing value: transition,animation-timing-function, or the newer linear() function for keyframe-based spring interpolation.
/* On a transition */
.card {
transform: scale(1);
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card:hover {
transform: scale(1.04);
}
/* On a keyframe animation */
.modal {
animation: slideUp 400ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}In React with inline styles you can build the string dynamically, which is useful for animation tools or for parameterising the spring feeling in a design system token:
// Design token approach
const easings = {
spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
snappy: 'cubic-bezier(0.77, 0.00, 0.18, 1)',
anticipate:'cubic-bezier(0.36, -0.30, 0.63, 1.3)',
}
// Dynamic spring strength
const spring = (strength = 1.56) =>
`cubic-bezier(0.34, ${strength}, 0.64, 1)`
// Usage in a component
style={{ transition: `transform 300ms ${spring(1.8)}` }}5. Five Curves Worth Bookmarking
After building the Bezier Editor and experimenting with motion for a while, these are the five I keep coming back to. Each one has a distinct personality. Hit Play on any of them to feel the difference.
The Default
easeThe browser default. Starts slightly fast and decelerates smoothly to a stop. Great for elements entering the screen, feels intentional without being dramatic.
cubic-bezier(0.25, 0.10, 0.25, 1.00)
The Workhorse
ease-outStarts at full speed, decelerates to rest. The most natural-feeling easing for UI elements moving into position, mimicking an object thrown and slowing down.
cubic-bezier(0.00, 0.00, 0.58, 1.00)
The Spring
springOvershoots the target slightly before snapping back. The P1 y-value above 1.0 is what makes it spring. One of the most satisfying curves for interactive UI, card hovers, and scale animations.
cubic-bezier(0.34, 1.56, 0.64, 1.00)
The Snap
snappyVery fast at the start, very slow at the end. Great for UI that needs to feel responsive: a drawer, a tooltip, a dropdown. The element snaps into place and drifts the last few pixels.
cubic-bezier(0.90, 0.00, 0.10, 1.00)
The Anticipate
anticipateDips slightly before launching forward, then overshoots at the end. The P1 y-value below 0 creates the anticipation. Playful and expressive, best used for hero elements and deliberate call-to-action animations.
cubic-bezier(0.36, -0.30, 0.63, 1.30)
Where to Go Next
The best way to develop an intuition for easing is to experiment visually. Adjust a control point and immediately see its effect on a moving element. You can do exactly that in the Bezier Editor Drag the handles, pick a preset, tweak the values numerically, and copy the CSS directly into your code.
- โStart with ease-out. Replace every linear or ease with ease-out on entering elements. The difference is immediate.
- โAdd a spring to hover states. Use cubic-bezier(0.34, 1.56, 0.64, 1) on scale or translate transitions on interactive cards and buttons.
- โMatch duration to distance. Longer distances need longer durations. A tooltip 8px away should be 120ms. A modal sliding in from offscreen should be 350โ450ms.
- โAvoid ease-in for UI. Ease-in feels like a loading animation, not a UI interaction. Keep it for exits.
- โUse overshoot sparingly. One spring per interaction is usually enough. Multiple overshooting elements compete for attention.