Always cooking something
โ† All posts
March 18, 2026ยท7 min read

Easing Curves Explained: The Secret Behind Great Animation

CSSAnimationMotion

Two animations can have the exact same duration and move between the exact same positions, yet one feels mechanical and cheap while the other feels physical and alive. The difference is almost always the easing curve.

Most developers never give them a second thought. They ship with ease, linear, orease-in-out and call it done. Not because it's a good choice, but because it's the path of least resistance. The result is interfaces that technically animate but never quite feel alive.

In this article we'll go deeper than the defaults. We'll break down how cubic-bezier curves actually work, what gives motion its physical quality, and how far you can push a single timing function to make an interface feel hand-crafted.


1. What Is a cubic-bezier?

A cubic-bezier is a mathematical curve defined by four points: a fixed start at (0, 0), a fixed end at (1, 1), and two movable control pointsP1 and P2. In CSS, you only specify the two control points:

css
transition-timing-function: cubic-bezier(x1, y1, x2, y2);
/*                                         โ”€โ”€P1โ”€โ”€  โ”€โ”€P2โ”€โ”€  */

The horizontal axis represents time (0 = start, 1 = end of the transition). The vertical axis represents progress (0 = initial value, 1 = final value). The curve maps time to progress; the slope at any point on the curve is the speed of the animation at that moment.

Steep slope = fast. A steep part of the curve means a lot of progress happens in a short time, meaning the animation is moving quickly. A flat section means almost no progress: the animation is nearly still.
Interactive ยท Time vs. Progress

Progress over time

Same 1200ms. The curve controls how fast it moves at each moment.

Notice how linear covers equal distance per unit time: predictable, but lifeless. ease starts a bit faster and decelerates. ease-out begins at full speed and coasts to a stop, which feels more natural because it mimics real-world friction.


2. Understanding the Four Values

Each control point has an x and a y coordinate. There's one important constraint: the x values must stay in the [0, 1] range. The y values, however, can go anywhere, and that's where things get interesting.

css
cubic-bezier(x1, y1, x2, y2)
/*
  x1, x2 โ†’ must be in [0, 1]   (time constraints)
  y1, y2 โ†’ can be anything      (progress can overshoot)

  P1 (x1, y1) โ†’ influences the start of the curve
  P2 (x2, y2) โ†’ influences the end of the curve
*/

P1 is connected to the start point (0, 0) with a line, and P2 is connected to the end point (1, 1). These lines are the handles. Pulling a handle away from the diagonal increases the curve's speed in that region. Pushing it toward the diagonal flattens the motion.

The five named CSS easings are just shortcuts for specific cubic-bezier values:

css
ease         โ†’ cubic-bezier(0.25, 0.10, 0.25, 1.00)
ease-in      โ†’ cubic-bezier(0.42, 0.00, 1.00, 1.00)
ease-out     โ†’ cubic-bezier(0.00, 0.00, 0.58, 1.00)
ease-in-out  โ†’ cubic-bezier(0.42, 0.00, 0.58, 1.00)
linear       โ†’ cubic-bezier(0.00, 0.00, 1.00, 1.00)
Interactive ยท Easing Comparison

Same duration (900ms), different curves. Watch where each ball is at the midpoint.

linear
ease-in
ease-out
ease-in-out
spring

The most striking comparison here is ease-in vs ease-out. Ease-in starts slow and accelerates, like an object being pushed from rest. For UI elements leaving the screen that can work, but for elements entering, it always feels sluggish. Ease-out is almost always the better default for entering elements.


3. Going Beyond [0, 1]: Overshoot and Anticipate

When a y value exceeds 1.0, the animation briefly surpasses its target before coming back. When a y value drops below 0, the animation briefly goes in the opposite direction before correcting. These are called overshoot and anticipate.

This is where CSS cubic-bezier gets genuinely expressive. A slight overshoot tricks the eye into reading the motion as physical, like a rubber band or a spring. Without any overshoot, even a well-crafted ease can feel like a computer moving something. With a subtle overshoot, it suddenly feels alive.

css
/* Standard ease-out โ€” stops exactly at the target */
transition-timing-function: cubic-bezier(0.00, 0.00, 0.58, 1.00);

/* Spring โ€” overshoots then snaps back */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1.00);
/*                                              ^^^^
                             y1 above 1.0 = overshoot */

/* Anticipate โ€” dips before launching */
transition-timing-function: cubic-bezier(0.36, -0.30, 0.63, 1.30);
/*                                              ^^^^^
                             y1 below 0 = moves in reverse briefly */
Interactive ยท Overshoot Slider
P1 y value (overshoot)1.56

Clear overshoot: physical and alive.

cubic-bezier(0.34, 1.56, 0.64, 1)

Drag the slider upward past 1.0 and watch the ball briefly fly past its destination before snapping back. Around 1.4โ€“1.6 is the sweet spot for most UI. Above 1.8, it starts feeling like a bounce and draws too much attention to itself.

Overshoot on opacity doesn't work. If you apply an overshooting curve to a property that can't go negative or beyond its range (like opacity), the browser clamps the value and the overshoot is invisible. Use overshoot on spatial properties: transform, left, width, scale.

4. Using cubic-bezier in Practice

You can use cubic-bezier anywhere CSS accepts an easing value: transition,animation-timing-function, or the newer linear() function for keyframe-based spring interpolation.

css
/* On a transition */
.card {
  transform: scale(1);
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card:hover {
  transform: scale(1.04);
}

/* On a keyframe animation */
.modal {
  animation: slideUp 400ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes slideUp {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

In React with inline styles you can build the string dynamically, which is useful for animation tools or for parameterising the spring feeling in a design system token:

js
// Design token approach
const easings = {
  spring:    'cubic-bezier(0.34, 1.56, 0.64, 1)',
  snappy:    'cubic-bezier(0.77, 0.00, 0.18, 1)',
  anticipate:'cubic-bezier(0.36, -0.30, 0.63, 1.3)',
}

// Dynamic spring strength
const spring = (strength = 1.56) =>
  `cubic-bezier(0.34, ${strength}, 0.64, 1)`

// Usage in a component
style={{ transition: `transform 300ms ${spring(1.8)}` }}

5. Five Curves Worth Bookmarking

After building the Bezier Editor and experimenting with motion for a while, these are the five I keep coming back to. Each one has a distinct personality. Hit Play on any of them to feel the difference.

The Default

ease

The browser default. Starts slightly fast and decelerates smoothly to a stop. Great for elements entering the screen, feels intentional without being dramatic.

cubic-bezier(0.25, 0.10, 0.25, 1.00)

The Workhorse

ease-out

Starts at full speed, decelerates to rest. The most natural-feeling easing for UI elements moving into position, mimicking an object thrown and slowing down.

cubic-bezier(0.00, 0.00, 0.58, 1.00)

The Spring

spring

Overshoots the target slightly before snapping back. The P1 y-value above 1.0 is what makes it spring. One of the most satisfying curves for interactive UI, card hovers, and scale animations.

cubic-bezier(0.34, 1.56, 0.64, 1.00)

The Snap

snappy

Very fast at the start, very slow at the end. Great for UI that needs to feel responsive: a drawer, a tooltip, a dropdown. The element snaps into place and drifts the last few pixels.

cubic-bezier(0.90, 0.00, 0.10, 1.00)

The Anticipate

anticipate

Dips slightly before launching forward, then overshoots at the end. The P1 y-value below 0 creates the anticipation. Playful and expressive, best used for hero elements and deliberate call-to-action animations.

cubic-bezier(0.36, -0.30, 0.63, 1.30)


Where to Go Next

The best way to develop an intuition for easing is to experiment visually. Adjust a control point and immediately see its effect on a moving element. You can do exactly that in the Bezier Editor Drag the handles, pick a preset, tweak the values numerically, and copy the CSS directly into your code.

  • โ†’Start with ease-out. Replace every linear or ease with ease-out on entering elements. The difference is immediate.
  • โ†’Add a spring to hover states. Use cubic-bezier(0.34, 1.56, 0.64, 1) on scale or translate transitions on interactive cards and buttons.
  • โ†’Match duration to distance. Longer distances need longer durations. A tooltip 8px away should be 120ms. A modal sliding in from offscreen should be 350โ€“450ms.
  • โ†’Avoid ease-in for UI. Ease-in feels like a loading animation, not a UI interaction. Keep it for exits.
  • โ†’Use overshoot sparingly. One spring per interaction is usually enough. Multiple overshooting elements compete for attention.

// newsletter

Stay in the loop

New experiments, articles, and tools โ€” straight to your inbox. No spam, unsubscribe anytime.