EmpireUI
Get Pro
← Blog7 min read#cubic-bezier#easing#css animation

cubic-bezier in CSS: Custom Easing Functions Explained Simply

Master CSS cubic-bezier easing functions from scratch. Learn how the four control points work, when to ditch ease-in-out, and how to craft motion that actually feels good.

Abstract colorful gradient wave motion on dark background

Why the Built-In Easing Keywords Aren't Enough

Every CSS animation you've ever shipped uses some kind of timing function. Most devs just grab ease or ease-in-out and call it a day. And honestly, for a lot of UI work — a hover state on a button, a simple fade — that's totally fine.

But here's where it breaks down. The moment you're animating something that needs to feel physically plausible — a modal flying in from the bottom, a card snapping into a grid, a notification sliding away — the built-in keywords start feeling stiff. ease is fine. It's not *yours*.

That said, the keywords do map to real cubic-bezier values. ease is cubic-bezier(0.25, 0.1, 0.25, 1.0). ease-in-out is cubic-bezier(0.42, 0, 0.58, 1.0). Once you know that, you realise you're not doing anything magical by writing cubic-bezier() — you're just getting precise about what those numbers actually mean.

This article is about giving you that precision.

The Four Numbers: What P1 and P2 Actually Control

A cubic Bézier curve is defined by four points. Two of them — (0,0) and (1,1) — are fixed. They represent the start and end of your animation in both time and progress. You only control the other two: P1 and P2, each with an x and y coordinate.

So cubic-bezier(x1, y1, x2, y2) gives you those two middle control points. The x values represent *time* — how far through the duration you are. The y values represent *progress* — how far through the animation's value range you are. X is clamped between 0 and 1. Y isn't — which is how you get overshoot (values above 1) and anticipation (values below 0).

Quick aside: this is why cubic-bezier(0, 0, 1, 1) is linear. Both control points fall exactly on the straight diagonal from (0,0) to (1,1). No acceleration, no deceleration. Perfectly boring, occasionally exactly what you need.

Worth noting: because x values must stay between 0 and 1, you can't create a curve that goes backwards in time. That's a physical constraint, not a CSS limitation.

Reading the Curve: Fast Start, Slow End, and Everything Between

Here's how to build intuition quickly. Push P1 toward the top-left — high y, low x — and your animation starts fast. It burns through most of its progress early, then crawls to the finish. That's your classic ease-out feel.

Flip it around: push P1 toward the bottom-right — low y, high x — and you get ease-in. Starts slow, builds momentum, ends abruptly. Works well for exits. Things leaving the screen should accelerate out, not gently drift away.

In practice, cubic-bezier(0.34, 1.56, 0.64, 1) is one of the most useful values you're not using yet. That y1 value of 1.56 pushes past 1.0, which creates a subtle overshoot — the element slightly overshoots its target position before settling back. It's the difference between a UI that feels mechanical and one that feels alive. Try it on a modal entrance in 2026 and watch every designer on your team suddenly get very interested in what you changed.

The CSS looks straightforward once you've got a value to paste:

.modal {
  animation: slideUp 340ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

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

How to Dial In Your Own Curve Without Guessing

You don't eyeball cubic-bezier values. Nobody does, not even people who've been doing this for ten years. You use a visual tool. Chrome DevTools has one built in — click the curve icon next to any timing function in the Styles panel and drag the handles. It previews live. Use it.

For more control, the gradient generator and other tools over at Empire UI give you a feel for how values map to visual output. If you're building something stylistically rich — say, a glassmorphism card that pops open — having the right easing is what separates "cool" from "actually polished."

One more thing — when you find a curve you like, name it. CSS custom properties don't store functions directly, but you can use a Sass variable or a JS constant so you're not copying magic numbers everywhere:

// tokens/motion.js
export const EASING = {
  springOut:  'cubic-bezier(0.34, 1.56, 0.64, 1)',
  snappyIn:   'cubic-bezier(0.55, 0, 1, 0.45)',
  softInOut:  'cubic-bezier(0.45, 0, 0.55, 1)',
};

Then in your component you just reference EASING.springOut and you've got a motion system instead of scattered magic numbers.

Using cubic-bezier in React and Framer Motion

In plain CSS you drop cubic-bezier(...) directly into transition or animation. With Framer Motion in React, it's a first-class prop — you pass it to ease inside your transition object as an array of four numbers.

Look, the array form trips up a lot of people the first time. You're not passing a string. You're passing [x1, y1, x2, y2] as a JS array:

import { motion } from 'framer-motion';

export function Card({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: 0.35,
        ease: [0.34, 1.56, 0.64, 1],
      }}
      className="card"
    >
      {children}
    </motion.div>
  );
}

Same curve, same feel, fully compatible with whatever physics you'd want to replicate in pure CSS. This consistency matters when you're mixing animated React components with CSS-only transitions on the same page — you want the motion vocabulary to feel unified.

Worth noting: Framer Motion also has a spring type for its transition, which is often a better fit for physics-like motion than a bezier. But when you need exact, predictable timing — like syncing with audio or matching a design spec — cubic-bezier with a hard duration wins every time.

Common Mistakes (and the One That'll Bite You in Production)

The most common mistake is applying the same easing to both enter and exit animations. An element entering should usually ease *out* — fast start, slow landing — because it's arriving at rest. An element exiting should ease *in* — slow start, fast exit — because it's departing with urgency. Using ease-in-out for both makes everything feel equally mushy.

The production gotcha? Duration mismatch. A springy overshoot curve like cubic-bezier(0.34, 1.56, 0.64, 1) at 600ms looks physical and satisfying. The same curve at 150ms looks glitchy. The overshoot doesn't have time to read. Anywhere below around 200ms, keep your curves tight — no values above 1.0 on the y axis.

Honestly, the other thing nobody talks about is reduced motion. If a user has prefers-reduced-motion: reduce set, your lovingly crafted spring easing means nothing — you should be collapsing duration to near-zero or removing the transform entirely. Don't just skip the animation. Do it properly:

@media (prefers-reduced-motion: reduce) {
  .modal {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

Building a Motion System Around Bezier Curves

Once you stop treating easing as an afterthought, you start thinking in motion tokens. Most design systems in 2026 define three to five standard curves — one for entrances, one for exits, one for UI feedback (button presses, toggles), and optionally one for hero-level cinematic moments.

If you're building on top of Empire UI, the box shadow generator and the component system are already opinionated about visual style. Your motion system should match that energy. A neobrutalism interface wants snappy, linear-ish curves with no overshoot. A vaporwave aesthetic can handle more dramatic spring effects. Motion should speak the same visual language as the rest of your design.

The practical upshot: define your curves in one file, document what each one is for, and enforce them in code review. It sounds like overhead. It's not. After a month of consistent motion tokens, your UI will feel like it was built by one person instead of six — and that is worth every minute of the setup.

FAQ

Can the y values in cubic-bezier go above 1 or below 0?

Yes, only the y values. Going above 1 creates overshoot past the animation's end value; going below 0 creates an anticipation pullback at the start. The x values must stay between 0 and 1.

What's the difference between cubic-bezier and a spring animation?

A cubic-bezier runs for a fixed duration you define. A spring animation is physics-based — its duration depends on stiffness, damping, and mass. Use cubic-bezier when you need exact timing; use spring when you want natural motion that feels physically grounded.

Is cubic-bezier supported in all browsers?

Yes, universally. It's been supported since Chrome 4, Firefox 4, and Safari 3.1. You don't need a polyfill or fallback for any browser in active use today.

How do I pick good cubic-bezier values without a design background?

Open Chrome DevTools, inspect any element with a transition, and click the curve icon next to the timing value. Drag the handles and watch the preview. Easings.net is also a solid reference with named presets and live demos.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

CSS Custom Properties: Dynamic Theming and Animation TricksAdvanced CSS Custom Properties: @property, Animatable TokensMotion Design Tokens: Systematising Easing, Duration and DelayCSS Custom Easing: linear(), steps() and Beyond cubic-bezier