How to Calculate cubic-bezier for Natural Motion
Part of Timing Functions & Easing Curves in Core CSS Animation Fundamentals.
Problem: UI transitions that feel robotic or floaty
Default CSS keywords — linear, ease, ease-in-out — apply mathematically clean velocity curves that do not match how physical objects actually move. Elements slide without mass, snap to rest without deceleration, or overshoot without damping. The perceptual gap is sharpest on high-DPI displays and 120 Hz panels, where the extra frame resolution makes velocity discontinuities immediately visible.
Root cause: control-point values that ignore physical inertia
A CSS cubic-bezier is defined as cubic-bezier(x1, y1, x2, y2). The two control points P1 (x1, y1) and P2 (x2, y2) shape the interpolation curve between the start and end state. The X coordinates govern how quickly time progresses through the animation — they must remain within [0, 1]. The Y coordinates govern the output value at each time step and may exceed [0, 1] to create overshoot when applied to hardware-accelerated properties.
Natural motion requires a strong initial acceleration (high early velocity) followed by a deceleration phase that settles asymptotically at rest. When control points are chosen arbitrarily — or left at a generic keyword default — the resulting velocity curve produces flat plateaus or abrupt stops that have no physical analogue. The browser compositor samples this curve every 16.67 ms at 60 fps; every imprecise sample compounds the perceptual mismatch.
Step-by-step: mapping spring dynamics to control points
Deriving control points from physical intuition follows a consistent four-step process.
-
Choose the motion character. Decide between: a smooth deceleration (standard UI), a fast entry with slow settle (modal or sheet), or an elastic overshoot (micro-interaction feedback). Each maps to a different region of the control-point space.
-
Set P1 to control initial acceleration. A low
x1(close to 0) and a lowy1produces a slow ramp — appropriate for fades. A lowx1with a highy1fires an immediate velocity spike — matching the feel of a physical tap or spring release. Raisingx1delays the acceleration, producing a “wind-up” effect. -
Set P2 to control deceleration. A high
x2(close to 1) combined withy2 = 1.0creates a long, smooth approach to rest — the characteristic of Material Design’s standard easing. Reducingx2sharpens the final settle. For overshoot, sety2 = 1.0and pushy1above1.0(e.g.1.56). -
Tune duration for perceived mass. Heavier visual elements feel wrong at fast durations even with a correct curve. Establish a base duration tied to the element’s visual weight: small badges at 150–200 ms, panels and sheets at 300–400 ms, full-screen transitions at 450–600 ms.
Reference control-point table
| Behaviour | x1 |
y1 |
x2 |
y2 |
Duration range |
|---|---|---|---|---|---|
CSS ease |
0.25 | 0.1 | 0.25 | 1.0 | 200–400 ms |
| Material standard | 0.4 | 0.0 | 0.2 | 1.0 | 250–400 ms |
| Elastic overshoot | 0.34 | 1.56 | 0.64 | 1.0 | 400–600 ms |
| Sharp deceleration | 0.0 | 0.0 | 0.2 | 1.0 | 150–300 ms |
| Symmetrical ease-in-out | 0.42 | 0.0 | 0.58 | 1.0 | 300–500 ms |
Production code pattern
The snippet below demonstrates the two most common natural-motion variants alongside the spring-to-bezier utility that generates them programmatically. Every variant includes a prefers-reduced-motion fallback — this is non-negotiable for accessible production code.
/* Standard natural deceleration — compositor-safe */
.element {
/* Compositor promotion: keeps animation off the main thread */
will-change: transform;
/* Fast entry, long smooth settle: suits panels, drawers, modals */
transition: transform 0.35s cubic-bezier(0.4, 0.0, 0.2, 1.0);
}
/* Elastic overshoot — only valid on compositor properties */
/* Y1 > 1.0 overshoots the target value before settling */
.element--spring {
will-change: transform;
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1.0);
}
/* Reduced-motion: remove animation entirely for vestibular safety */
@media (prefers-reduced-motion: reduce) {
.element,
.element--spring {
transition: none;
}
}
Rendering impact: composite — both variants animate transform, which the GPU compositor handles without touching layout or paint.
/**
* Spring-to-bezier approximation utility.
* Maps stiffness and damping to cubic-bezier control points.
* This is a curve-fitting approximation, not an exact physics simulation.
* For true spring physics use the Web Animations API with a custom easing function.
*
* @param {number} stiffness - Spring tension (1–300; higher = faster initial velocity)
* @param {number} damping - Friction (1–100; higher = less overshoot)
* @returns {string} CSS cubic-bezier() declaration
*/
function springToBezier(stiffness = 100, damping = 15) {
// x1: delay before peak velocity — damping reduces the delay
const x1 = Math.min(0.9, 0.2 + damping / 100);
// y1: peak output value — stiffness drives how high it climbs (overshoot if > 1)
const y1 = Math.min(2.0, 0.1 + stiffness / 200);
// x2: time at which deceleration begins — inversely scaled by damping
const x2 = Math.max(0.1, 0.25 - damping / 200);
// y2: always ends at 1.0 (the target state)
const y2 = 1.0;
return `cubic-bezier(${x1.toFixed(2)}, ${y1.toFixed(2)}, ${x2.toFixed(2)}, ${y2.toFixed(2)})`;
}
// springToBezier(100, 15) → "cubic-bezier(0.35, 0.60, 0.18, 1.00)"
// springToBezier(200, 5) → "cubic-bezier(0.25, 1.10, 0.23, 1.00)" (overshoot)
// springToBezier(50, 80) → "cubic-bezier(0.90, 0.35, 0.10, 1.00)" (heavy damping)
Rendering impact: main-thread at calculation time (JavaScript), then composite once the resulting CSS value is applied to transform.
Verification checklist
Constraints and trade-offs
- X-axis strictly bounded. Browsers clamp or ignore
x1orx2values outside[0, 1]. Some implementations silently fall back tolinear; others produce console warnings. Always validate before shipping. - Overshoot on non-compositor properties causes layout overflow. Using
y1 > 1.0onwidth,height,top, orlefttemporarily pushes the geometry beyond its intended bounds, potentially causing scrollbar flicker or clipping by a parentoverflow: hiddencontainer. See avoiding layout thrashing in CSS animations for safe property alternatives. - No perfect spring approximation. A true damped spring equation has infinite theoretical duration. Cubic-bezier forces a hard end at the specified duration, which can produce a slight snap at the tail of high-stiffness, low-damping values. For pixel-perfect spring physics, use
KeyframeEffectwith the Web Animations API. - Mobile frame scheduling varies. WebKit and Blink handle composited animation differently on 60 Hz vs 120 Hz panels. A curve that feels smooth on desktop may appear clipped on ProMotion displays if the duration is too short for the refresh rate to express the easing shape. Test at both refresh rates.
will-changehas memory cost. Each promoted layer consumes GPU texture memory. Declarewill-changejust before the animation, then remove it after it completes — do not apply it globally in a reset stylesheet. See layer promotion and will-change strategy for budget guidance.
Related
- Timing Functions & Easing Curves — parent topic covering the full easing model and compositing tier
- Hardware-accelerated properties — which properties are safe for overshoot and why
- Avoiding layout thrashing in CSS animations — consequences of animating layout-triggering properties
FAQ
How do I convert a physical spring equation to a CSS cubic-bezier?
Cubic-bezier cannot perfectly represent a damped spring — a spring has continuous velocity that can overshoot indefinitely, while cubic-bezier is capped at a fixed duration. For a close approximation, map the damping ratio to x1 and the stiffness factor to y1, then validate by plotting the velocity curve. For true spring physics, use the Web Animations API with a custom easing function.
Can cubic-bezier Y-axis values exceed 1.0?
Yes. Values greater than 1.0 or less than 0 on the Y-axis create overshoot or undershoot effects, simulating elastic bounce. The X-axis must remain strictly within [0, 1] to prevent infinite evaluation and browser timing errors.
Why does my calculated curve feel different on mobile devices? Mobile browsers use different frame scheduling and touch-input latency compensation. GPU compositing behaviour also varies across WebKit and Blink, altering how sub-pixel rendering interpolates the easing curve. Test on target devices and adjust duration (±50 ms) to compensate for perceived speed.
What happens if I apply an overshoot curve to a layout-triggering property?
The geometry temporarily exceeds its intended bounds, causing scrollbar flicker, visible clipping by overflow: hidden parents, or cumulative layout shifts. Restrict overshoot to transform and opacity.