🎨 CSS

CSS Cubic Bezier Generator

Drag the two control points to design a custom easing curve, and watch the live preview.

About Cubic Bezier Easing

Drag the two handles to shape a custom cubic-bezier(x1, y1, x2, y2) timing curve for your animation or transition. X stays locked between 0 and 1 because it represents time, but Y is free to go past that range, which is how you get bounce and overshoot effects.

Why X and Y follow completely different rules

A CSS cubic-bezier timing function always starts at a fixed point (0, 0) and ends at a fixed point (1, 1), your two draggable handles are the curve's interior control points. The X axis represents elapsed time as a fraction of the animation's total duration, and since time can't run backward or exceed 100% of the duration, this tool clamps the X1 and X2 handles (and their number inputs) strictly between 0 and 1. The Y axis represents the eased output value, essentially "how far along" the animated property is, and that has no such restriction in the spec. Letting Y drop below 0 or rise above 1 is exactly what produces a bounce or overshoot: the property briefly animates past its final value (Y greater than 1) or dips before its starting value (Y less than 0), then settles back, which is how spring-like and anticipation-style motion effects are built from pure math rather than keyframe hacks.

How the SVG diagram maps to real bezier coordinates

The visual editor works in a 200×200 SVG coordinate space, but bezier math works in a 0-to-1 space, so every drag has to be converted between the two. Horizontally the mapping is straightforward, bezier x × 200 = svg x, but vertically it has to flip: SVG's y-axis grows downward from the top-left corner, while a bezier curve's y-axis conventionally grows upward, so the tool inverts it with svg y = 200 − (bezier y × 200). That's why dragging a handle up in the visual editor increases its Y value in the number input below, even though "up" and "increasing" would naturally align if the coordinate systems matched, which they don't without that inversion.

Why dragging feels reliable even during a fast flick

Each draggable circle calls setPointerCapture() the moment you press down on it. Without that call, a fast mouse or finger movement can slip outside the small SVG element's bounds mid-drag, at which point the browser would normally stop sending move events to that element entirely. Pointer capture keeps all subsequent pointer events routed to the handle you grabbed regardless of where the cursor physically travels, which is what makes it possible to drag a handle to an extreme overshoot position without the interaction breaking partway through.

The "Play Animation" preview isn't a simulation

Rather than calculating bezier positions frame-by-frame in JavaScript to fake the motion, clicking Play actually sets a real CSS transition property on the ball element using your exact cubic-bezier() value, then changes its left position, letting the browser's own animation engine handle the easing. That means what you see in the preview is pixel-for-pixel identical to how the curve will actually render in production, not an approximation, because it's the same rendering code path a real webpage would use.

The default curve is the built-in ease keyword in disguise

This tool's starting values, (0.25, 0.1, 0.25, 1), aren't arbitrary, they're the exact cubic-bezier equivalent of CSS's own ease keyword, the default timing function browsers apply when you don't specify one. For reference, linear is cubic-bezier(0, 0, 1, 1) (a straight, constant-speed line), ease-in is cubic-bezier(0.42, 0, 1, 1) (slow start), and ease-out is cubic-bezier(0, 0, 0.58, 1) (slow finish). Knowing these reference points is useful for judging how far your custom curve deviates from familiar, expected motion.

Frequently Asked Questions

Why can't I drag the X coordinate of a handle past 0 or 1?

X represents elapsed time as a fraction of the animation's duration, which can't logically go backward or beyond 100%. This tool enforces that constraint by clamping the X value during dragging and via min/max limits on the number inputs, matching how the CSS spec itself treats cubic-bezier's time axis.

Why can the Y value go below 0 or above 1?

Y represents the eased output progress, not time, and the CSS spec places no limit on it. Values outside 0-1 produce overshoot or bounce effects, where the animated property briefly passes beyond its final value or dips before its start before settling, which is how spring-like motion is created.

Is the "Play Animation" ball movement a real preview or an approximation?

It's real. The tool applies your exact cubic-bezier() value as an actual CSS transition-timing-function on the ball element and lets the browser's native animation engine render it, so what you see matches production behavior exactly rather than a JavaScript-simulated approximation.

What do the default starting values (0.25, 0.1, 0.25, 1) represent?

They're the exact cubic-bezier equivalent of CSS's built-in ease keyword, the default timing function browsers use automatically when no timing function is specified. Starting from these values gives you a familiar baseline to adjust from.

What's the difference between animation-timing-function and transition-timing-function in the copied output?

Both accept the identical cubic-bezier() syntax and produce the same easing math, but they apply to different CSS features: animation-timing-function controls @keyframes-based animations, while transition-timing-function controls property transitions triggered by state changes like :hover. The tool outputs both so you can use whichever matches your use case.

Why does the visual editor need to flip the Y axis when converting drag positions?

SVG coordinates grow downward from the top-left corner, while bezier curve values conventionally grow upward. The tool inverts the vertical mapping mathematically so dragging a handle up on screen correctly increases its Y value in the generated cubic-bezier() output.