Easing
CSS easing functions explained, with cubic-bezier values
What ease, ease-in, ease-out and linear really do, how the four cubic-bezier numbers work, steps(), overshoot, named curves and which easing to use when.
An easing function decides how an animation spends its time. Two animations can move the same distance in the same 600 ms and feel completely different: one lands softly, one feels mechanical, one overshoots and settles. The only thing that changed is the easing.
This page is a reference for the easing functions CSS gives you: the five keywords and their exact cubic-bezier() values, how the four numbers work, steps(), overshoot curves, a table of well-known named curves, and practical rules for picking one. For springs and bounces, which a Bezier curve cannot draw, see the companion article on the CSS linear() function.
Each ball travels the full track in 1.2 seconds. Halfway through the time, they are in very different places: ease-in has covered about 31% of the distance, ease-out about 68%, the default ease about 80%, and easeOutBack has already passed the end and is about to come back. Each runner in the demo uses the easing named on its row:
.ez-run { width: calc(100% - 18px); } /* track width minus the ball */
.ez-go .ez-run { animation: ez-move 1200ms var(--ez) 200ms both; }
@keyframes ez-move { to { transform: translateX(100%); } }
The keywords and their exact values
Every CSS easing keyword except linear is shorthand for a cubic Bezier curve. These are the values from the CSS Easing specification:
| Keyword | Equivalent | Feels like |
|---|---|---|
linear | constant speed | Mechanical. Right for spinners, progress, colour cycles |
ease (default) | cubic-bezier(.25, .1, .25, 1) | Quick start, long gentle stop |
ease-in | cubic-bezier(.42, 0, 1, 1) | Slow start, full speed at the end |
ease-out | cubic-bezier(0, 0, .58, 1) | Full speed at the start, slow landing |
ease-in-out | cubic-bezier(.42, 0, .58, 1) | Slow at both ends, symmetric |
step-start | steps(1, jump-start) | Jumps to the end immediately |
step-end | steps(1, jump-end) | Holds, then jumps at the end |
The keyword ease-in and ease-out curves are mild. Most design systems use stronger curves, which is why named curves like easeOutCubic exist.
How the four numbers work
cubic-bezier(x1, y1, x2, y2) draws a curve from (0, 0) to (1, 1). The horizontal axis is time, from the start of the animation to the end. The vertical axis is progress, from the start value to the end value. The four numbers are the two control points that bend the curve between those fixed ends:
- (x1, y1) is the first control point. It shapes how the animation starts.
- (x2, y2) is the second control point. It shapes how the animation ends.
The slope of the curve is the speed. A steep start means the value changes quickly at first; a flat end means it creeps into place. That is why ease-out, with its first control point at (0, 0) and second at (.58, 1), starts steep and ends flat.
The x values must be between 0 and 1, because time cannot go backwards. If you write cubic-bezier(1.2, 0, .5, 1), the declaration is invalid and the browser ignores it. The y values have no such limit.
Overshoot and anticipation
When a y value goes above 1, the curve rises past the end value before coming back: the element overshoots its target and settles. When a y value goes below 0, the element first moves slightly backwards, which animators call anticipation.
/* overshoot: y2 above 1 */
.pop { transition: transform 400ms cubic-bezier(.34, 1.56, .64, 1); }
/* anticipation: y1 below 0 */
.pull { transition: transform 400ms cubic-bezier(.36, 0, .66, -.56); }
A Bezier curve can overshoot once. It cannot wobble several times or bounce, because it has only two control points. For that you need linear() with many stops or a JavaScript spring.
Overshoot works best on transform. On opacity it does nothing visible above 1, and on width or height it causes layout to jump around the element.
steps()
steps(n, position) splits the animation into n equal jumps with no motion in between. It is how sprite-sheet animations, typewriter effects and ticking clocks are made.
| Position | Behaviour |
|---|---|
jump-end (default), end | Holds the start value; the last jump lands at the end of the duration |
jump-start, start | Jumps immediately; the end value is reached at the last step |
jump-none | n values including both start and end, each held for an equal time |
jump-both | Jumps at both the start and the end, n + 1 jumps in total |
The steps(6) row in the demo uses the default jump-end: six jumps, and the ball reaches the end of the track at the very end of the 1.2 seconds. For a sprite sheet with 8 frames of 64 px in a row, animate background-position-x from 0 to -512px (the full sheet width) with steps(8): each frame shows once, and the jump back to 0 at the end of each loop is the first frame again. With steps(8, jump-none) you would end at -448px, the last frame, instead.
Well-known named curves
Many design tools and animation libraries use the names from Robert Penner's easing equations. The originals are polynomial and exponential functions; the cubic-bezier versions below are the widely used approximations listed on easings.net.
| Name | cubic-bezier | Use |
|---|---|---|
| easeOutQuad | (.5, 1, .89, 1) | Light, subtle entrances |
| easeOutCubic | (.33, 1, .68, 1) | A good default for anything entering |
| easeOutQuart | (.25, 1, .5, 1) | Stronger deceleration, panels and drawers |
| easeOutExpo | (.16, 1, .3, 1) | Very fast start, long settle. Popular for modals |
| easeOutBack | (.34, 1.56, .64, 1) | About 10% overshoot. Toasts, badges, snap-backs |
| easeInCubic | (.32, 0, .67, 0) | Elements leaving the screen |
| easeInOutCubic | (.65, 0, .35, 1) | Moving something from one place to another |
| easeInOutQuart | (.76, 0, .24, 1) | Same, with a more pronounced middle |
Store the ones you use as custom properties so the whole interface shares one vocabulary:
:root {
--ease-out: cubic-bezier(.33, 1, .68, 1);
--ease-in: cubic-bezier(.32, 0, .67, 0);
--ease-in-out: cubic-bezier(.65, 0, .35, 1);
--ease-back: cubic-bezier(.34, 1.56, .64, 1);
}
.toast { transition: transform 300ms var(--ease-back), opacity 200ms var(--ease-out); }
Which easing to use when
- Entering: ease-out. Things arriving should decelerate into place, like an object coming to rest. The user's eye catches the fast start and the element is readable sooner.
- Leaving: ease-in. Things leaving should accelerate away. The slow start shows where it is going; the fast end gets it out of the way.
- Moving on screen: ease-in-out. An element going from one visible position to another, such as a reordered card or a sliding tab indicator, should start and stop gently.
- Continuous or mechanical: linear. Spinners, progress bars, marquees and colour cycles. Anything else looks robotic with linear.
- Playful feedback: overshoot. Use sparingly, on small elements, for confirmations and snap-backs.
When the same element enters and leaves, use different transitions for each direction. In CSS, put the entering transition on the open state and the leaving one on the base state, because the transition that applies is the one declared on the state being moved to.
Duration by distance and size
Easing and duration work together. A strong ease-out with a short duration feels snappy; the same curve over a second feels sluggish. These ranges are a reasonable starting point for interface motion:
| Change | Duration |
|---|---|
| Colour, opacity, small icon changes | 100 to 200 ms |
| Buttons, toggles, tooltips, small movements under 100 px | 150 to 250 ms |
| Dropdowns, cards, toasts, movement of a few hundred px | 250 to 400 ms |
| Modals, drawers, full-screen transitions | 300 to 500 ms |
Larger elements and longer distances need more time, but not proportionally more: doubling the distance should add perhaps 30 to 50% to the duration. Exits can be about 20% shorter than entrances. On phones, where distances are smaller, stay near the low end.
Find the easing on a real site
When an animation on another site feels right, the curve is usually the reason, and it is hard to judge by eye. You can read it from Chrome DevTools, and this guide covers the cases where DevTools does not show it, such as JavaScript animations. The Capture Motion extension records the easing, duration and keyframes of every animated property on the page, from CSS, the Web Animations API, GSAP or Lottie, and gives them back as CSS, React or GSAP code, JSON, or a Figma Motion timeline.
Questions
What is the default easing in CSS?
ease, which is cubic-bezier(.25, .1, .25, 1). It applies to transitions and animations when you do not set a timing function. It starts a little fast and slows down for most of its length.
Can cubic-bezier values be negative or above 1?
The two y values can be any number, which is how you get anticipation (below 0) and overshoot (above 1). The two x values must stay between 0 and 1, or the whole declaration is invalid.
What is the difference between ease-out and ease-in?
ease-out starts fast and slows down at the end, so it suits things arriving. ease-in starts slow and speeds up, so it suits things leaving. The names describe which end of the motion is gentle.
How do I find the easing a website uses?
Inspect the element in Chrome DevTools and look at transition-timing-function or animation-timing-function, or use the Animations panel. The Capture Motion extension reads the curve of every animated property for you.
