CSS easing and cubic-bezier curves, made intuitive
When something moves on screen, the path it takes is not the only thing that matters. How its speed changes over time is what makes motion feel natural or robotic. That speed curve is the easing function, and in CSS it is set by the transition-timing-function and animation-timing-function properties. Get it right and a simple fade feels considered; get it wrong and everything feels mechanical.
What an easing function actually is
An easing function maps progress in time to progress in the animation. Both run from 0 to 1. At time 0 the animation is at its start; at time 1 it is at its end. The function decides everything in between. A straight line means constant speed. A curve means the element accelerates or decelerates along the way.
Crucially, easing does not change where the animation ends or how long it takes. It only changes the pacing within that window.
The built-in keywords
CSS ships with named functions that cover most needs:
- linear — constant speed start to finish. Honest but lifeless for most UI. Good for spinners and progress bars.
- ease — the default. Starts quickly, then slows. A reasonable all-rounder.
- ease-in — starts slow, speeds up. Things feel like they are leaving, so it suits exits and elements moving off screen.
- ease-out — starts fast, slows down. Things feel like they are arriving and settling, so it suits entrances and most UI feedback.
- ease-in-out — slow at both ends, fast in the middle. Smooth for moves that both start and stop on screen.
.panel {
transition: transform 250ms ease-out;
}
If you reach for one default, ease-out is usually the safest for interface motion because the eye cares most about how a movement settles.
How cubic-bezier maps to a curve
Every one of those keywords is really a cubic-bezier() underneath. The function takes four numbers:
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
The curve always starts at the point (0, 0) and ends at (1, 1). Those endpoints are fixed. The four numbers are the coordinates of two control points that pull the curve into shape: cubic-bezier(p1x, p1y, p2x, p2y). The first pair is the handle near the start; the second pair is the handle near the end.
The x value of each control point is its position in time and must stay between 0 and 1. The y value is its position in the animation. A control point pulled upward early (high first y) makes the start fast; pulled flat early makes the start slow. The standard keywords map to fixed coordinates, for example ease-out is cubic-bezier(0, 0, 0.58, 1).
Overshoot with values above one
The y values are not limited to the 0 to 1 range. Pushing a control point's y above 1 (or below 0) makes the curve overshoot its endpoint and swing back, which reads as a small bounce or springy settle.
.chip {
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
Here the second control point's y of 1.56 pulls the curve past the target before it returns, so the element overshoots slightly and settles. Used sparingly on entrances and toggles, this adds life. Used everywhere, it becomes noise.
When to use which
- Entrances, dropdowns, things arriving: ease-out or a gentle overshoot.
- Exits, dismissals: ease-in.
- Looping or mechanical motion: linear.
- Custom personality: a hand-tuned
cubic-bezier.
Reading four numbers off a curve is hard to do in your head, so build it visually. The Cubic Bezier Generator lets you drag the control points, preview the motion, and copy the value. When you animate a gradient or color alongside the move, the CSS Gradient Generator and Color Converter & Contrast Checker keep the rest of the styling in step.