cubic-bezier()

Gabriel Shoyombo on

Get affordable and hassle-free WordPress hosting plans with Cloudways — start your free trial today.

The cubic-bezier() function lets you define custom timing curves for CSS animations and transitions. It gives you precise control over how elements move. Beyond basic easing, it unlocks wild effects like elastic bounces, reverse flows, or multi-step pacing, all within four numeric values.

.box {
  width: 100px;
  transition: width 0.4s cubic-bezier(0.65, 0, 0.35, 1);

  &:hover {
    width: 200px; /* Expands naturally */
  }
}

The cubic-bezier() function is defined in the CSS Easing Functions Level 2 specification

Syntax

cubic-bezier( [ <number [0,1]>, <number> ]#{2} )

Arguments

property: cubic-bezier(x1, y1, x2, y2);

The cubic-bezier() function takes four arguments — including two control points (x1, y1 and x2, y2) — defining a curve on a 0-to-1 grid. It’s used in transition-timing-function or animation-timing-function. For example:

transition: all 1s cubic-bezier(0.25, 0.1, 0.25, 1);
  • x1, x2: Horizontal positions (time) between 0 and 1.
  • y1, y2: Vertical positions (progress), can exceed 01 for overshoot/bounce.

The cubic-bezier() function shapes timing with a Bézier curve. The x values (time) stay 01, but y values (progress) can overshoot for dynamic effects. Tools like cubic-bezier.com can be used to visualize it.

Basic usage

Let’s start with cubic-bezier() in action. It can be used in any timing function value, such as transition-timing-function and animation-timing-function.

For instance, if I wanted to create a button that slides to the right when hovered, but with a playful, elastic feel, like it’s stretching and snapping into place, I could use the transform: translateX(0) to set the button’s starting position at its natural spot. On hover, transform: translateX(100px) moves it 100px to the right. The transition: transform 0.6s cubic-bezier(0.68, -0.6, 0.32, 1.6) is where the magic happens

.button {
  transform: translateX(0);
  transition: transform 0.6s cubic-bezier(0.68, -0.6, 0.32, 1.6);

  &:hover {
    transform: translateX(100px); /* Elastic slide */
  }
}

The cubic-bezier(0.68, -0.6, 0.32, 1.6) curve does the heavy lifting: 0.68, -0.6 (first control point) makes the button pull back slightly at the start (negative y1 means it moves “backward” in progress), and 0.32, 1.6 (second control point) pushes it past the endpoint (overshooting to 1.6, above 100% progress) before settling at 100px. This creates an elastic effect, like a rubber band snapping forward.

Equivalent to easing keywords

All easing function keywords (e.g., ease-in or ease-out) are no more than presets for the cubic-bezier(), and have an equivalent in its coordinate system.

  • ease-in: Equivalent to cubic-bezier(0.42, 0, 1, 1)
  • ease-out: Equivalent to cubic-bezier(0, 0, 0.58, 1)
  • ease-in-out: Equivalent to cubic-bezier(0.42, 0, 0.58, 1)
  • ease: Equivalent to cubic-bezier(0.25, 0.1, 0.25, 1)

Reverse motion

Let’s make a div that grows in width but with a “rewind” effect as if it’s pulling back before finishing with cubic-bezier(). The .rewind element starts at width: 100px, and on hover, it expands to width: 200px. The transition: width 0.8s cubic-bezier(0.95, 0.05, 0.8, -0.5) controls the motion, while the 0.8s duration makes it slow enough to see the effect.

.rewind {
  width: 100px;
  transition: width 0.8s cubic-bezier(0.95, 0.05, 0.8, -0.5);

  &:hover {
    width: 200px; /* Grows with a reverse pull */
  }
}

The cubic-bezier(0.95, 0.05, 0.8, -0.5) curve shapes the timing. 0.95, 0.05, the first control point, starts the growth very fast (close to 1 on the time axis, with low progress), then slows down dramatically. The 0.8, -0.5, second control point, pulls the progress below 0% near the end (negative y2), making the width shrink slightly before settling at 200px. This creates a rewind-like motion — fast growth, a pause, and a slight retraction before finishing.

Elastic bounce

Another animation you can achieve with cubic-bezier() is an elastic bounce with a square div that scales up when clicked but with an exaggerated, bouncy feel, like a cartoon button. The .bounce div starts at transform: scale(1) (its normal size, 100px × 100px). When clicked (:active), it grows to transform: scale(1.3), which is 130% of its size. The transition: transform 0.5s cubic-bezier(0.8, -0.5, 0.2, 1.8) defines the animation, and the 0.5s duration keeps it quick but visible.

.bounce {
  transform: scale(1);
  transition: transform 0.5s cubic-bezier(0.8, -0.5, 0.2, 1.8);

  &:active {
    transform: scale(1.3); /* Big elastic bounce */
  }
}

The cubic-bezier(0.8, -0.5, 0.2, 1.8) curve creates the bounce. 0.8, -0.5 (first control point) starts fast but pulls back slightly (negative y1 means the scale dips below 1, like a squash), and 0.2, 1.8 (second control point) overshoots the target (1.8 exceeds 100% progress, scaling past 1.3) before settling at 1.3. This makes the div squash, stretch, and bounce back, mimicking a springy effect.

Multi-stage easing

For this demo, I wanted a div to slide and fade in dramatically, in a staged way, like it’s speeding up, pausing, then rushing to finish. The .stage div starts at transform: translateX(0) (no movement) and opacity: 0.5 (semi-transparent). On hover, it moves to transform: translateX(150px) (150px to the right) and opacity: 1 (fully opaque). The transition: all 1s cubic-bezier(0.1, 0.9, 0.9, 0.1) applies the effect to both properties over one second, long enough to see the stages.

.stage {
  transform: translateX(0);
  opacity: 0.5;

  transition: all 1s cubic-bezier(0.1, 0.9, 0.9, 0.1);

  &:hover {
    transform: translateX(150px);
    opacity: 1; /* Fast start, slow middle, fast end */
  }
}

The cubic-bezier(0.1, 0.9, 0.9, 0.1) curve creates an S-shape. 0.1, 0.9 (first control point) starts very fast (low x1, high y1 means rapid early progress), then 0.9, 0.1 (second control point) slows down in the middle (high x2, low y2 means progress stalls) before speeding up to finish.

Staggered flow

To make a list of items to fade in and slide up one after another, with a smooth, overshooting motion, like they’re popping into place with a little bounce, as in a staggered flow, cubic-bezier() comes in handy. Each .flow-item starts at opacity: 0 (invisible) and transform: translateY(30px) (30px below its final spot). The @keyframes flow moves it to opacity: 1 (fully visible) and transform: translateY(0) (its natural position). The animation: flow 0.7s cubic-bezier(0.5, 0, 0, 1.5) forwards runs the animation over 0.7s, and forwards ensures the final state sticks.

.flow-item {
  opacity: 0;
  transform: translateY(30px);
  animation: flow 0.7s cubic-bezier(0.5, 0, 0, 1.5) forwards;
}

.flow-item:nth-child(2) {
  animation-delay: 0.15s;
}
.flow-item:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes flow {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

The cubic-bezier(0.5, 0, 0, 1.5) curve shapes the motion. 0.5, 0 (first control point) starts slowly (low progress early), and 0, 1.5 (second control point) overshoots (1.5 exceeds 100% progress), so items slide past their final spot (slightly above) before settling back. The animation-delay (0s, 0.15s, 0.3s) staggers the items, creating a wave-like flow.

cubic-bezier() vs. Other timing functions

Here’s how cubic-bezier() compares to ease, linear, and steps():

cubic-bezier()easelinearsteps()
PurposeCustom timing curve with 4 control points.Smooth start/end, preset curve.Constant speed, no easing.Discrete jumps, no smooth flow.
Syntaxcubic-bezier(0.4, 0, 0.2, 1)easelinearsteps(4, end)
BehaviorAdjustable. It can be smooth, bouncy, or staged.Preset gentle easing.Straight-line progression.Choppy, step-by-step changes.
FlexibilityHigh. Provides endless curve possibilities.Low — fixed curve.Low — no variation.Moderate — step count adjustable.
Use CaseElastic buttons, reverse slides, custom flow.Default smooth transitions.Even-paced changes (e.g., timers).Typewriter effects, frame-by-frame.
ExampleOvershoots with (0.8, -0.5, 0.2, 1.8).Subtle ease-in/out.Steady fade.Jumpy shifts.

Demo

For a quick demo, let’s see a simple example of a box that grows in width on hover, with a smooth, natural-feeling animation. The .box starts at width: 100px and expands to width: 200px when hovered. The transition: width 0.4s cubic-bezier(0.65, 0, 0.35, 1) controls the effect. The 0.4s duration makes it quick but noticeable. The cubic-bezier(0.65, 0, 0.35, 1) curve shapes the timing, with the first control point starting slowly, and then the second control point speeds up toward the end, landing exactly at 100% progress.

.box {
  width: 100px;
  transition: width 0.4s cubic-bezier(0.65, 0, 0.35, 1);

  &:hover {
    width: 200px; /* Expands naturally */
  }
}

Browser support

cubic-bezier() is supported in all browsers since 2011. No fallbacks needed, presets like ease cover older cases.

Specification

The cubic-bezier() function is defined in the CSS Easing Functions Level 2 specification, which is currently in Editor’s Draft.

Additional reading