
Cubic-Bezier Easing — How to Make CSS Animations That Feel Right
📷 Pixabay / PexelsCubic-Bezier Easing — How to Make CSS Animations That Feel Right
Most CSS animations look stiff because they use the wrong easing. A practical look at cubic-bezier curves, how to read them, when to overshoot, and why linear() changed the game in 2024.
The first time I tried to make a card animation feel smooth, I spent an afternoon tweaking transition-duration and got nowhere. The card moved from one position to another in 300ms. Then 500ms. Then 200ms. None of them felt right. The animation was technically correct, the timing was reasonable, but the motion felt cheap — like a stage prop on a wire rather than a card being placed on a table.
The problem was not the duration. It was the easing. I was using ease, the default value, which is a fine starting point but not actually right for most things. The fix was a six-character change: cubic-bezier(0.4, 0, 0.2, 1). The same animation, same duration, suddenly felt like it had weight.
If you have ever stared at a CSS animation that looked stiff or fake and could not figure out why, the answer is probably the easing curve. This guide is about how to think about cubic-bezier curves, which ones actually feel right in different contexts, and where the format falls short.
The companion tool is the CSS Cubic Bezier Generator, which lets you drag the control points around and see the curve and a live preview in real time. I built it because doing this purely from the four numbers in the function call is impossible to feel intuitively.
What a Cubic-Bezier Curve Actually Is
The math under cubic-bezier comes from Pierre Bezier, a French engineer who developed the curve format at Renault in the 1960s for car body design. The form he settled on uses two anchor points (start and end) and two control points that pull the curve toward them.
In CSS, the start anchor is fixed at (0, 0) and the end anchor at (1, 1). The X axis is time — 0 at the start of the animation, 1 at the end. The Y axis is the progress of the animated property — 0 means no progress, 1 means complete. The two control points you specify are X-Y coordinates that bend the curve.
So cubic-bezier(0.4, 0, 0.2, 1) says:
- Start at (0, 0)
- First control point at (0.4, 0)
- Second control point at (0.2, 1)
- End at (1, 1)
The first control point sits low on the curve (Y = 0), pulling the early part of the animation flat. The second control point sits high (Y = 1), pulling the later part of the animation flat. The middle of the curve has the steepest slope. That means the property changes slowly at first, fast in the middle, and slowly at the end. This is what we mean by "ease-in-out" — except this specific curve weights the deceleration more heavily, which is the Material Design standard for forward motion.
You do not need to memorize the math. What you do need is intuition for how curve shape maps to perceived motion.
Reading the Curve Shape
Open the CSS Cubic Bezier Generator and drag the points around. The patterns are remarkably consistent.
A curve that starts flat and ends steep = slow start, fast finish = "ease-in." Things accelerating away from you. Use this for elements leaving the screen — a modal closing, a notification dismissing, an item moving to the trash. The motion gets fast as it goes, which matches our expectation that something exiting should pick up speed.
A curve that starts steep and ends flat = fast start, slow finish = "ease-out." Things arriving and settling. Use this for elements appearing — modals opening, tooltips, page transitions in. The motion starts quickly (catching the eye) and decelerates as it lands (settling into place). This is the most commonly correct easing for UI in my experience.
An S-curve = slow start, fast middle, slow end = "ease-in-out." Things in transit between two visible states. Use for cards sliding, page sections rearranging, drawer panels moving. Both the takeoff and the landing are smooth.
A nearly straight diagonal line = linear. Constant speed. Use almost never for UI motion. Linear feels mechanical because real-world objects do not move at constant speed — friction, gravity, and inertia mean things speed up and slow down. The two correct uses for linear are: continuous loops where there is no start or end to feel (a spinner, a marquee, a progress indicator that loops), and pure data visualization (a value increasing in lockstep with a number it represents).
A curve that briefly dips below 0 or above 1 = overshoot. Used for "back" easing where the element pulls back slightly before moving forward, or overshoots its target before settling. Useful for attention-grabbing moments. Overused everywhere else.
The Curves I Actually Use
After enough years of writing CSS animations, I have settled on a small palette of curves that cover ninety percent of what I need.
Standard ease-out: cubic-bezier(0.0, 0.0, 0.2, 1)
The Material Design "deceleration" curve. My default for anything entering. Modals, tooltips, dropdown menus, fade-ins.
Standard ease-in: cubic-bezier(0.4, 0.0, 1, 1)
The Material Design "acceleration" curve. For things exiting. Closing modals, dismissing notifications, deleting items.
Standard ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1)
The Material Design "standard" curve. For motion between two visible states. Drawer panels, card movement, view transitions.
Soft ease-out: cubic-bezier(0.16, 1, 0.3, 1)
A more pronounced deceleration than the standard. Feels more "premium" or relaxed. I use this for hero elements and large transitions where I want the motion to feel deliberate.
Anticipate: cubic-bezier(0.68, -0.55, 0.27, 1.55)
Pulls back slightly before going forward, then overshoots before landing. Use sparingly for attention-grabbing moments — a "success" check appearing, a confetti animation, a toast notification you really want noticed. Do not use this on any UI that the user will see hundreds of times.
That is basically it. Maybe one or two custom curves per project for specific brand moments. The rest should default to one of these.
When Cubic-Bezier Falls Short
Here is the limitation of cubic-bezier: it is one curve, with two control points. It cannot do certain things that real motion does.
It cannot do real spring physics. A spring oscillates — it overshoots, comes back, overshoots a little less, comes back, and gradually settles. That is multiple oscillations in a single motion. A cubic-bezier can do at most one overshoot. If you want the kind of bouncy, settling animation you see in iOS, you cannot do it with cubic-bezier alone. You need either the linear() function (more on that below) or a JS animation library that does spring math.
It cannot represent multi-stage motion. "Move halfway, pause briefly, then continue" is not expressible as a single cubic-bezier. You need keyframes for that, or chained transitions.
It cannot do steps or snaps. "Jump to 25%, hold, jump to 50%, hold" is what steps() is for. Cubic-bezier always produces continuous motion.
The four numbers are unreadable. If I show you cubic-bezier(0.16, 1, 0.3, 1) cold, you cannot picture the curve. This is the biggest practical problem with the format. Tools like the CSS Cubic Bezier Generator exist precisely to make this format usable. The numbers without a visual preview are nearly meaningless.
The linear() Function — The 2024 Game-Changer
In late 2023 and rolling out through 2024, browsers shipped support for a new easing function: linear(). Despite the name, it is not actually linear — it lets you specify any easing curve as a series of points, sampled by the browser between them.
Syntax:
transition-timing-function: linear(0, 0.05, 0.2, 0.4, 0.7, 0.9, 1);
Each number is a Y value at evenly-spaced X positions from 0 to 1. The browser interpolates linearly between each point — but with enough points, you can approximate any curve, including spring physics with multiple oscillations.
Tools like Linear Easing Generator (an open-source project from Jake Archibald) let you draw a spring curve or import a Framer Motion spring config and export a linear() value with as many sample points as you want. This is genuinely revolutionary for CSS animation — for the first time, you can do real spring motion in pure CSS without JavaScript.
When to reach for linear() over cubic-bezier():
- You want a spring motion (multi-bounce settle).
- You want a precise match to a curve from a design tool that exports JSON or SVG path data.
- You want to chain easing stages within a single transition.
When to stick with cubic-bezier():
- The motion is simple and one-shot.
- You want the CSS to be readable when you come back to it in six months.
- You are matching an existing design system that uses cubic-bezier curves.
I still default to cubic-bezier for ninety-five percent of UI animation. The cases where linear() is the right call are real but specific.
Steps and the Other Easing Functions
CSS has a few easing options beyond cubic-bezier and linear that are worth knowing.
steps(n, jump-start | jump-end) — Snaps to discrete values rather than animating smoothly. Useful for spritesheet animations, segmented progress bars, frame-by-frame animation. steps(8, end) advances in 8 discrete jumps over the duration.
step-start and step-end — Jumps the value immediately at the start or end. Useful for instantaneous state changes you still want to coordinate with a duration.
ease, ease-in, ease-out, ease-in-out — Named keywords that map to specific cubic-bezier values. They are convenient defaults but the actual curves are mediocre — ease is cubic-bezier(0.25, 0.1, 0.25, 1), which is fine but not as good as the Material Design curve. I rarely use these named keywords once I am comfortable writing cubic-bezier directly.
The Animation Curve Is Half the Animation
Once you have a curve picked, the CSS Animation Generator is a useful next step — it gives you a full keyframe animation with the curve applied, plus the CSS to copy. For complex multi-step animations where keyframes matter more than easing, that tool is more useful.
If your animation involves color transitions, the CSS Gradient Generator can help you preview how an interpolated background-color animation will look — gradients use the same color interpolation as transitions.
For path-based animations and clip reveals, the CSS Clip Path Generator covers the geometry side. Combine a clip-path animation with a well-chosen cubic-bezier and you can make remarkably polished reveal effects.
Common Pitfalls I Have Watched Devs Hit
Using linear for everything because it seems "simple." Linear is the worst default. Pick any of the named easings (ease-out is a safe bet) before you pick linear.
Animating with the default transition: all and surprising yourself. The shorthand transition: all 300ms will animate every property that changes, including ones you did not mean to (height changes, color changes, you name it). Be explicit: transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1) only animates transform.
Using overshoot easings for boring transitions. A back-easing on a button color change is silly. Save overshoot for moments that should feel celebratory or attention-grabbing.
Forgetting that durations and curves interact. A 100ms ease-out feels different from a 500ms ease-out. The same curve at different durations reads as different animations. Tune both together.
Animating layout properties with cubic-bezier and wondering why it is janky. Cubic-bezier does not change what you animate, only how it eases. Animating width or height causes layout reflow and is jittery regardless of easing. Animate transform: scale() or transform: translate() instead — those are GPU-composited and stay smooth.
Letting designers and developers use different curves. If your design system specifies easing tokens, both Figma and the CSS should use them. The most common visual mismatch between mockups and implementation is unmatched easing curves. Standardize.
Accessibility — prefers-reduced-motion
Some users get nausea, dizziness, or headaches from animation. The browser exposes their preference through a media query:
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
The nuclear approach above shortens every animation to be effectively instant. A more graceful approach is to swap dramatic easings for simpler ones, drop overshoot animations entirely, and shorten durations:
@media (prefers-reduced-motion: reduce) {
.modal {
transition: opacity 100ms linear;
}
}
The prefers-reduced-motion media query is supported in every browser at this point. There is no excuse for not respecting it. I check this on every project — it takes ten minutes and meaningfully improves the experience for the people who need it.
Building an Easing Vocabulary
Animation easing is a vocabulary, not a technical specification. Once you internalize "this curve = ease-out, used for entrances," "this curve = ease-in, used for exits," the cubic-bezier numbers fade into the background and you start thinking about motion in terms of intent.
The CSS Cubic Bezier Generator is the fastest way to build that vocabulary. Drag the points around. Compare two curves side by side. Try the same curve at 100ms and 500ms and 1000ms. Save the ones that feel right to you and reuse them.
The animations on toolboxhubs.com use a curated set of about six curves. Every transition pulls from that set. The result is a consistent feel across the entire site — different elements all moving in a coordinated way, even though the elements themselves vary. That coordination is what separates a polished interface from a piecewise one. And it starts with picking the curves you trust.