← Blog

2026.10.12 · 10 min · animation · css · motion

Easing: why some motion feels right.

Two boxes cross the same distance in the same time and one of them feels alive. A short, practical tour of easing functions: what the curve means, which one to reach for, and how to write any of them in CSS today.

Put two boxes on two identical rails. Give them the same distance and the same 2 seconds. One moves at a constant speed. The other starts slowly, picks up speed in the middle and settles into the stop. Nothing else is different, and yet one reads as a machine and the other as an object.

SAME DISTANCE · SAME TIMELINEAREASE-IN-OUTFIG. 01 / SAME DISTANCE, SAME TIME

The difference is the easing function: the rule that decides how far along the movement should be at each moment. Every animation has one, even when nobody chose it. This post is what I wish someone had shown me before I spent years tweaking durations to fix motion that was actually wrong in its curve.

I first ran into this when I started animating collapsible sidebars. The panel opened and closed at a perfectly reasonable speed and still felt stiff, and no duration I tried fixed it.

Linear is the default nobody wants

When you interpolate between two values the obvious way, you get linear motion:

linear.ts
const lerp = (from: number, to: number, t: number) => from + (to - from) * t;

t runs from 0 to 1 over the duration, and the value moves the same amount in every frame. The speed jumps from zero to full in the first frame and from full to zero in the last. Nothing physical does that. Anything with mass has to accelerate and decelerate, and our eyes have spent our whole lives watching things that do.

Animators named this a long time ago. Slow in and slow out is one of the twelve principles Disney's animators Ollie Johnston and Frank Thomas laid out in The Illusion of Life (1981): put more drawings near the start and end of a movement and fewer in the middle, so the action eases into and out of its key poses. An easing function is that principle written as math.

An easing function is one line of math

An easing function takes the fraction of time that has passed and returns the fraction of the movement that should be done. Both run from 0 to 1:

ease.ts
type Ease = (t: number) => number;
 
const tween = (from: number, to: number, t: number, ease: Ease) =>
  from + (to - from) * ease(t);
 
const linear: Ease = (t) => t;
const easeInCubic: Ease = (t) => t ** 3;
const easeOutCubic: Ease = (t) => 1 - (1 - t) ** 3;
const easeInOutCubic: Ease = (t) => (t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2);

The only change from lerp is the highlighted line: time goes through ease before it reaches the value. Same start, same end, same duration. Only the distribution of the movement over that duration changes.

Reading the curve

Plot ease(t) with time on the horizontal axis and progress on the vertical one and you get the curve every easing tool shows you. The one skill worth learning is this: the slope is the speed. Where the curve is flat, the object is barely moving. Where it is steep, it is moving fast.

TIME →10PROGRESSEASE-IN-OUT CUBICTRACKt 1.00 · p 1.00FIG. 02 / EASE-IN-OUT CUBIC

The dots on the curve are spaced evenly in time: one every twelfth of the duration. Project them across onto the track and you see where the object actually is at each of those moments. They bunch up at both ends (slow in, slow out) and spread out in the middle (fast). Move your pointer along the graph to scrub through time and watch the ball on the track.

Three things to take from the picture:

  • Ease-in starts flat and ends steep: slow start, fast finish. It is the shape of something falling or leaving.
  • Ease-out starts steep and ends flat: fast start, gentle stop. It is the shape of something arriving and settling.
  • Ease-in-out is both, an S-curve: still at each end, fastest in the middle.

The families

Most easing libraries today ship the same set of curves, and almost all of them trace back to one person. Robert Penner published his easing equations for Flash in 2001, under a BSD licence, and they spread into practically every animation tool since: jQuery, Qt's QEasingCurve, GSAP, and the cheat sheet at easings.net. When a library offers you easeInOutQuart, that is Penner's naming.

Each family comes in in, out and in-out versions:

FamilyEase-in formulaCharacter
Sine1 - cos(t·π/2)The gentlest curve; barely distinguishable from linear at short durations.
Quad, cubic, quart, quintt ** 2 … t ** 5One family with rising power. Each step makes the ends slower and the middle faster.
Expo2 ** (10t - 10)Extreme: nearly still, then all at once. expo.out is a great reveal.
Circ1 - sqrt(1 - t ** 2)Like expo, with a harder corner.
Backovershoots below 0 (in) or above 1 (out)Pulls back before it goes, or goes past and returns.
Elasticdecaying sineA spring that oscillates around the target.
Bouncepiecewise parabolasHits the target and rebounds in shrinking hops.

The out version of any curve is the in version flipped: out(t) = 1 − in(1 − t). The in-out version runs in on the first half and out on the second, each squeezed into half the time. Once you have one direction, you have all three.

GSAP renames the power curves, which trips people up: power1 is quad, power2 is cubic, power3 is quart and power4 is quint. So the gold box in figure 1 is power2.inOut, the same as easeInOutCubic.

Which one to reach for

This is where easing stops being math and becomes design. My working rules, which are also the motion rules of this site's design system:

  • Things that enter: ease-out. A panel opening, a word rising out of a mask, a toast sliding in. Ease-out moves the most in the first frames, so the interface feels like it responded immediately, then it settles quietly. Here that is expo.out for reveals.
  • Things that leave: ease-in. An element on its way out can start slowly, because nobody is waiting for it, and accelerate off the screen.
  • Things that move from one place to another on screen: ease-in-out. A tab indicator sliding to the next tab, a card reordering. Both ends are visible, so both ends should be calm. Here that is power3.inOut for state changes.
  • Things tied to input: linear. When scroll position or a drag drives the animation, the person is the easing. Adding a curve on top makes the content lag behind their finger. Everything scrubbed on this site uses ease: "none".
  • Things that repeat forever: linear. A spinner or a marching dashed line should not speed up and slow down once per loop.

And one rule about duration, because the two interact: easing changes how a duration feels. An ease-out at 240 ms feels quicker than linear at 240 ms because most of the distance is covered early. I use three durations, 140, 240 and 480 ms, and fix the curve before I touch the timing.

Shaping the feel

Swapping the curve changes the character of a movement more than any other parameter. Here is the same graph with ease-out bounce. The ease-in-out curve from figure 2 is the dashed line, for comparison.

TIME →10PROGRESSEASE-OUT BOUNCETRACKt 1.00 · p 1.00FIG. 03 / EASE-OUT BOUNCE

The bounce reaches the target at about 36% of the duration (exactly 1/2.75, a constant from Penner's formula), then falls back and returns three times, each hop smaller. On the track, the ball arrives fast and rattles to a stop. It is fun once. In a product interface it gets tiring by the third time, which is why I keep bounce, elastic and back for moments that are meant to be noticed and never for routine state changes. This site uses none of them.

Easing in CSS

CSS has had easing since transitions arrived, in three forms.

Keywords and cubic-bezier(). The keywords are shorthands for cubic Bézier curves:

KeywordEquivalent
ease (the default)cubic-bezier(0.25, 0.1, 0.25, 1)
ease-incubic-bezier(0.42, 0, 1, 1)
ease-outcubic-bezier(0, 0, 0.58, 1)
ease-in-outcubic-bezier(0.42, 0, 0.58, 1)

The four numbers are the x and y of the two control points of a Bézier curve from (0, 0) to (1, 1). The x values must stay between 0 and 1, because time can't run backwards. The y values can go outside that range, and that is how CSS does overshoot:

overshoot.css
.card {
  transition: transform 480ms cubic-bezier(0.34, 1.56, 0.64, 1); /* back-out: goes past, comes back */
}

Notice that the default, ease, is not ease-in-out. It is an asymmetric curve that front-loads the motion, closer to an ease-out. If you never set a timing function, that is what you have been shipping.

linear(), for everything a Bézier can't do. A single cubic Bézier can't bounce or oscillate, because it can't revisit a value more than once in the way a spring does. The linear() function, despite its name, fixes that: it takes a list of progress values and joins them with straight segments. With enough points, any curve is possible. It shipped in Chrome and Edge 113, Firefox 112 and Safari 17.2, so it works in every current browser.

You don't write those points by hand. Sample any JavaScript easing function and print the result:

to-linear.ts
const toLinear = (ease: (t: number) => number, samples = 40) =>
  `linear(${Array.from({ length: samples + 1 }, (_, i) => +ease(i / samples).toFixed(3)).join(", ")})`;
 
toLinear(easeOutBounce);
// "linear(0, 0.005, 0.019, 0.042, … , 0.996, 1)"

Paste the string into transition-timing-function or animation-timing-function and the bounce from figure 3 runs entirely in CSS, on the compositor, without a JavaScript animation library.

steps(), for frames. steps(8, end) jumps between eight discrete values instead of interpolating. It is the tool for sprite sheets, typewriter effects and clock hands.

The Web Animations API takes the same strings, so element.animate(keyframes, { duration: 240, easing: "cubic-bezier(0, 0, 0.58, 1)" }) behaves exactly like the CSS transition.

Easing doesn't make motion optional

A well-chosen curve makes motion feel natural. It doesn't make it welcome to everyone. Some people get dizzy or nauseous from movement on screen, and operating systems let them say so. Respect prefers-reduced-motion: drop the travel and keep the outcome. On this site every GSAP animation sits inside gsap.matchMedia().add("(prefers-reduced-motion: no-preference)", …), and every figure is drawn in its final state, so with motion off you still see what happened.

The short version

  1. An easing function maps time (0 to 1) to progress (0 to 1). Only the distribution changes, never the endpoints or the duration.
  2. On the graph, slope is speed.
  3. Ease-out to enter, ease-in to leave, ease-in-out to move, linear when the input or a loop drives it.
  4. Fix the curve before the duration.
  5. cubic-bezier() covers most curves. linear() covers the rest, in every current browser.

I built a small playground for exactly this: ease.a2coder.dev. It has 28 curves, from linear to bounce, an adjustable duration, and a preview that plots the curve as it plays. Pick one and press play. You will feel the difference before you can explain it.

I built it because finding the right Bézier values for micro-interactions was always the slow part for me, and the defaults were never quite satisfying. Also, building small tools like this is a hobby.


Sources: MDN, <easing-function> · Chrome for Developers, Create complex animation curves in CSS with linear() · Robert Penner's easing equations, as attributed in Qt · easings.net · GSAP eases · Twelve basic principles of animation · MDN, prefers-reduced-motion