Home / Articles / Animation & Motion /
cubic-bezier(): custom easing curves
The named keywords cover common cases, but custom curves let you craft animations that feel uniquely alive — bouncy, snappy, or dramatically weighted.
How cubic-bezier() works
A cubic Bezier curve is defined by four numbers — two control points, each with an x and y coordinate. The x axis represents time (0 to 1) and the y axis represents progress (0 to 1).
/* cubic-bezier(x1, y1, x2, y2) */
/* The named keywords as cubic-bezier values */
ease: cubic-bezier(0.25, 0.1, 0.25, 1.0)
ease-in: cubic-bezier(0.42, 0.0, 1.0, 1.0)
ease-out: cubic-bezier(0.0, 0.0, 0.58, 1.0)
ease-in-out: cubic-bezier(0.42, 0.0, 0.58, 1.0)
linear: cubic-bezier(0.0, 0.0, 1.0, 1.0)
The x values must stay between 0 and 1 (they represent time, which can't go backwards). The y values can exceed that range — that's how you create overshoot and bounce effects.
Snappy deceleration
The most universally useful custom curve: a fast start with a long, gentle slowdown. It feels responsive because the element moves immediately.
.panel {
transform: translateX(-100%);
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.panel.is-open {
transform: translateX(0);
}
The y1 value of 1 means the element covers most of its distance early, then eases into its final position. This is the go-to curve for slide-in panels and modals.
Overshoot and spring
When y2 exceeds 1, the animation overshoots its target before settling back. This creates a spring-like bounce.
/* Subtle overshoot */
.badge {
transform: scale(0);
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.badge.is-visible {
transform: scale(1);
/* Scales slightly past 1, then settles back */
}
/* Dramatic spring */
.notification {
transform: translateY(-20px);
opacity: 0;
transition:
transform 0.5s cubic-bezier(0.17, 1.67, 0.29, 0.97),
opacity 0.3s ease;
}
.notification.show {
transform: translateY(0);
opacity: 1;
}
y1 or y2 create increasingly exaggerated bounces. Keep them under 1.8 for UI — beyond that it looks glitchy.Slow-in for dramatic reveals
A strong ease-in starts slow and accelerates. Use it for elements that should build anticipation — a hero image loading in, or a counter reaching its value.
.counter {
/* Slow start, fast finish */
transition: all 1.2s cubic-bezier(0.7, 0, 0.84, 0);
}
/* More dramatic: barely moves at first, then rushes */
.reveal {
transition: transform 0.8s cubic-bezier(0.85, 0, 0.15, 1);
/* This is actually ease-in-out on steroids */
}
Strong ease-in curves are rare in UI because they feel sluggish. The element doesn't respond immediately, so users may think nothing happened. Reserve them for decorative animations.
Building a curve library
Define your curves as custom properties for consistency across your project. Name them by character, not by math.
:root {
--ease-snappy: cubic-bezier(0.22, 1, 0.36, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-smooth: cubic-bezier(0.45, 0, 0.55, 1);
--ease-dramatic: cubic-bezier(0.85, 0, 0.15, 1);
--ease-spring: cubic-bezier(0.17, 1.67, 0.29, 0.97);
}
.card {
transition: transform 0.3s var(--ease-snappy);
}
.modal {
transition: transform 0.4s var(--ease-bounce);
}
This keeps your motion design consistent. When you want to adjust the feel of all bouncy animations, you change one custom property.
The new linear() function
For curves that cubic-bezier can't express — multi-bounce, elastic, or stepped — modern CSS offers linear() with arbitrary control points.
/* A multi-bounce elastic effect */
.elastic {
transition: transform 0.6s linear(
0, 0.22, 0.78, 1.15, 1, 0.94, 1, 0.98, 1
);
}
/* Each value is the progress at evenly-spaced time points.
Values above 1 = overshoot, below previous = bounce back */
The linear() function interpolates linearly between the given points. With enough points, you can approximate any curve — including true spring physics that cubic-bezier cannot express.