CSS easing functions: what cubic-bezier actually does
By Flavio Copes
Learn how CSS easing works under the hood: the four cubic-bezier control points, what ease and ease-out really are, plus linear() and steps().
Every CSS transition and animation has an easing function. It decides how a value moves from start to end over time.
You’ve used ease and ease-out a hundred times. But what are they, really?
They’re all cubic-bezier() curves. Once you understand the four numbers inside, you can read any easing value and design your own.
The curve
Picture a graph. Time goes on the horizontal axis, from 0 to 1. Progress goes on the vertical axis, also from 0 to 1.
A straight diagonal line means constant speed: at 50% of the time, the animation is 50% done. That’s linear.
Easing functions bend this line. A curve that starts flat and gets steep means the animation starts slow and speeds up. A curve that starts steep and flattens out means it starts fast and slows down.
The four control points
cubic-bezier() takes four numbers:
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
These are the coordinates of two points: (x1, y1) and (x2, y2).
The curve always starts at (0, 0) and ends at (1, 1). Those two points in between are handles that pull the curve towards them, like the pen tool in a design app.
- the first point shapes the start of the curve
- the second point shapes the end
A y value higher than the x value pulls the curve up (faster progress early). A y value lower than x pulls it down (slower progress early).
The x values must stay between 0 and 1, because time can’t go backwards. The y values can go below 0 or above 1, and that’s where things get fun. More on that in a moment.
The keywords are just bezier curves
The named easings map directly to cubic-bezier() values:
ease /* cubic-bezier(0.25, 0.1, 0.25, 1) */
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) */
Look at ease-in: the first point is (0.42, 0). The y is 0 while x is 0.42, so the curve stays flat at the start. Slow start, fast finish.
ease-out is the mirror: (0.58, 1) as the second point means the curve flattens at the end. Fast start, slow finish.
ease is not symmetric. It accelerates quickly and decelerates for most of its duration. That’s why it feels more natural than ease-in-out for many UI movements.
Why linear feels robotic
Nothing in the physical world moves at constant speed. A ball you throw decelerates. A door you push accelerates then slows down as you catch it.
Our brains expect that. When a box slides across the screen at perfectly constant speed, it reads as mechanical, like a printer head.
My advice: use ease-out for elements entering the screen. They arrive fast and settle gently, which feels responsive. Use ease-in for elements leaving, and ease-in-out for elements that move from one place to another.
Reserve linear for things that genuinely are mechanical: loading spinners, marquees, progress bars.
You can compare all of these side by side in the easing preview tool, which draws the curve for each function and animates them together so you can feel the difference.
Going beyond 0 and 1: overshoot
Remember how the y values can exceed the 0–1 range? That’s how you get overshoot.
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
The second point has y = 1.275. The curve goes past the final value before coming back. A box scaling up will grow slightly too big, then snap back to its final size.
This is the classic “back” easing, and it makes buttons and modals feel springy without any JavaScript.
A negative y on the first point does the opposite: the element pulls back slightly before moving forward, like winding up before a throw:
transition-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045);
The linear() function
A cubic bezier curve can only bend so much. It can overshoot once, but it can’t bounce three times.
That’s what the newer linear() function is for. Despite the name, it’s not constant speed. It lets you define easing as a series of points, and the browser connects them with straight lines:
animation-timing-function: linear(
0, 0.36, 0.63, 0.85, 1,
0.88 65%, 1, 0.98 85%, 1
);
With enough points, those straight segments approximate any curve, including a real bounce or a spring simulation. This used to require JavaScript animation libraries. Now it’s a CSS value.
Each value is the progress at that moment, and the optional percentage sets when it happens. Values without a percentage are spaced evenly.
You won’t write these by hand. Tools generate the point list from a spring or bounce formula, and you paste the result in.
steps()
The last easing family is steps(). Instead of a smooth curve, it jumps between a fixed number of frames:
animation-timing-function: steps(8);
The animation holds still, then snaps to the next position, 8 times total.
This is how you animate a sprite sheet, or build a typewriter effect, or a ticking clock hand:
.second-hand {
animation: tick 60s steps(60) infinite;
}
@keyframes tick {
to {
transform: rotate(360deg);
}
}
The hand jumps once per second instead of sweeping. Same keyframes, completely different feel, all from the timing function.
Where easing applies
One thing that trips people up: easing applies between keyframes, not across the whole animation.
If your animation has keyframes at 0%, 50% and 100%, an ease-out timing function eases out twice — once per segment. Keep that in mind when an animation with several keyframes feels strangely jerky.
If you want a refresher on where these timing functions plug in, I wrote about the basics in CSS transitions and CSS animations. The timing function is the same property in both, and it’s the single biggest lever you have on how motion feels.
Related posts about css: