tutorials6 min read

Understanding CSS cubic-bezier(): A Visual Guide

What cubic-bezier() actually controls, why the built-in easing keywords look different from each other, and how to read or build your own custom curve.

Adesh Shukla··Updated 7 July 2026

Every transition and animation you've written has a timing function attached to it, even if you never typed one — ease is the default. Most of the time that's fine and you move on. But the moment you need something that feels less generic — a button that overshoots slightly before settling, or a card that snaps in fast and eases out slowly — ease-in-out stops being can start to feel limiting. For many UI transitions, cubic-bezier(x1, y1, x2, y2) gives you direct control over how the motion accelerates and decelerates without reaching for an animation library.

What the four numbers actually are#

A CSS cubic bezier easing curve is defined by two control points, P1 = (x1, y1) and P2 = (x2, y2), plotted against a graph where the x-axis is time (0 to 1, start to end of the animation) and the y-axis is progress (0 to 1, how far through the animated property's change you are). The curve always starts at (0, 0) and ends at (1, 1) — those two points are fixed and not part of what you write. The four numbers you supply are the two control points that pull the curve between them.

.el {
  transition: transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);
}

That's the exact curve ease-in-out expands to. Here's what the five built-in keywords actually resolve to, if you've ever wondered why they feel different:

KeywordEquivalent
linearcubic-bezier(0, 0, 1, 1)
easecubic-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)

Reading these directly is genuinely hard — nobody looks at 0.25, 0.1, 0.25, 1 and pictures the curve. That's what a visual editor is for: drag the two control points and watch the curve (and the animation it drives) change in real time. I built one into this site's CSS Shapes & Animation Playground for exactly that reason — every animated demo on that page lets you pick a preset easing or drag your own curve and see the cubic-bezier() output update live.

The x-axis is restricted, the y-axis isn't#

This is the detail that trips people up: x1 and x2 must stay between 0 and 1. Values outside that range make the cubic-bezier() easing function invalid. The reason is that the x-axis represents input progress through time, so the curve needs to map each moment in the animation to a single progress value. y1 and y2 have no such limit. A y value greater than 1 or less than 0 is completely valid, and it's how you get overshoot.

/* "Back" easing — overshoots past 100% before settling */
.el {
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

A y1 of 1.56 means the curve's progress briefly exceeds 100% partway through, then eases back down to 1. Visually, that's the element growing slightly past its target size and settling back — the "back" easing pattern you'll recognize from a lot of native iOS/macOS UI. There's no keyword for this; cubic-bezier() with an out-of-range y value is the only way to express it in plain CSS.

Reading a curve without dragging it#

If you're staring at four numbers and need a rough mental model without opening a tool:

  • Where the curve leaves the x-axis late (a low y1 held for a while) → a slow start, most motion happens near the end. That's an "ease-in" shape.
  • Where the curve reaches the top early (a high y2 reached quickly) → a fast start that trails off. That's "ease-out."
  • A steep middle, shallow ends → the classic "ease-in-out" S-curve — slow, fast, slow.
  • y values outside 0–1 → overshoot or anticipation (a slight pull backward before moving forward, if y1 is negative).

None of that replaces actually looking at the curve, but it's usually enough to guess whether you want to nudge a value up or down.

When to reach for something other than cubic-bezier()

A single cubic-bezier() gives you one continuous easing curve for the whole transition. It can create acceleration, deceleration, anticipation, and overshoot, but it cannot model a true physics-based spring that reacts dynamically to velocity or interruption.


For more complex motion, you might use CSS keyframes, the Web Animations API, linear() for multi-point easing curves, or a JavaScript animation library with spring physics.


For most everyday UI transitions — buttons, cards, menus, modals, and hover states — cubic-bezier() is often more than enough. True spring behavior and interruption-aware motion are different problems and usually need a different approach.

A few curves worth keeping around#

These four cover most of what "make this feel less robotic" actually needs, in my experience:

/* Snappy, confident — fast out, no overshoot. Good default for hover states. */
cubic-bezier(0.16, 1, 0.3, 1);
 
/* Anticipation — pulls back slightly before moving forward. Good for playful UI. */
cubic-bezier(0.68, -0.55, 0.27, 1.55);
 
/* Back — overshoots the target then settles. Good for modals/toasts appearing. */
cubic-bezier(0.34, 1.56, 0.64, 1);
 
/* Smooth deceleration only — no ease-in. Good for content that's already moving (drag release). */
cubic-bezier(0, 0.55, 0.45, 1);

The fastest way to find out whether one of these is right for your specific case is still to look at it against your actual element, at your actual duration — a curve that reads as "snappy" over 200ms can read as "abrupt" over 600ms with nothing else changed. That's the whole reason a drag-to-build editor is worth more here than a table of numbers.

A

Adesh Shukla

Frontend developer with a design background. Building DevStash — a developer ecosystem covering automation, AI workflows, and modern frontend systems.

Related Posts