Overshoot and Anticipation With cubic-bezier(): Motion With Character
The standard easing keywords — ease, ease-in, ease-out, ease-in-out — are polite. They start and stop smoothly and never go anywhere they were not told to. That is the right default, but it makes everything feel the same. Two techniques borrowed from character animation add personality with a single curve: overshoot, where motion carries a little past its target and settles back, and anticipation, where motion dips backwards before it launches. Both come from pushing cubic-bezier() control points outside the 0–1 range. This page shows the exact values, the physical intuition behind them, and — just as important — where they make an interface feel worse. It is part of Easing & Motion Design within the CSS-Only Micro-Interactions & Animations guide.
Why character matters, and when it does not
Real objects have mass. A drawer that slams open overshoots on its runners; a person about to jump crouches first. When interface elements borrow those cues, they feel as though they obey the same physics as the world, and users read them as more responsive and more deliberate. The effect is small but measurable in how "snappy" an interface feels.
The same cues become noise when overused. An overshoot on every hover, every dropdown and every tooltip makes an interface feel bouncy and imprecise; anticipation on a menu makes it feel slow, because the menu starts by going the wrong way. The working rule is to reserve character easing for moments that deserve emphasis — an element arriving, a state being confirmed — and keep routine, frequent interactions on neutral curves.
There is also an accessibility angle. Overshoot and anticipation add movement that is not strictly necessary. For users who ask for reduced motion, both should drop back to a plain curve or no transition.
Reading the four numbers
cubic-bezier(x1, y1, x2, y2) describes a curve from (0, 0) to (1, 1) with two control points. The x values are time and must stay between 0 and 1. The y values are progress and may go anywhere. The first control point shapes the start of the motion; the second shapes the end.
- Overshoot: set
y2above 1. The end of the curve bulges past the target, so progress exceeds 100% briefly and returns.cubic-bezier(0.34, 1.56, 0.64, 1)peaks at roughly 110% — a clear but brief overshoot. - Anticipation: set
y1below 0. The start of the curve dips beneath the origin, so progress goes briefly negative before accelerating forward.cubic-bezier(0.36, -0.6, 0.66, 1)pulls back about 8% before launching. - Both:
cubic-bezier(0.68, -0.6, 0.32, 1.6)does both, which reads as springy and theatrical. Use it sparingly.
The size of the overshoot in pixels is the progress overshoot multiplied by the distance travelled. A 10% overshoot on a 16-pixel icon is under 2 pixels; on a 320-pixel drawer it is 32 pixels, which is too much. Scale the curve's strength inversely with distance.
The complete implementation
The demo shows the same card-arrival and card-dismissal with three easing tokens. Watch the arrival row for overshoot and the dismissal row for anticipation.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Overshoot and anticipation</title>
<style>
:root {
/* Easing tokens: name the intent, not the numbers. */
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1); /* neutral arrival */
--ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1); /* emphatic arrival */
--ease-anticipate: cubic-bezier(0.36, -0.6, 0.66, 1); /* wind-up departure */
}
body { font: 15px/1.5 system-ui, sans-serif; margin: 1.5rem; display: grid; gap: 1rem; }
.lane {
container-type: inline-size; /* so the travel distance can use cqi */
position: relative;
height: 3rem;
border-radius: 8px;
background: #f1f5f9;
overflow: hidden;
}
.chip {
position: absolute;
inset-block: 0.5rem;
left: 0.5rem;
width: 5rem;
border-radius: 6px;
background: #4f46e5;
color: #ffffff;
display: grid;
place-items: center;
font-size: 0.8rem;
animation: arrive 1.8s infinite alternate;
}
/* The chip travels across the lane and back. alternate makes the
return trip play the curve in reverse, so overshoot on arrival
becomes anticipation on departure automatically. */
@keyframes arrive {
from { translate: 0 0; }
to { translate: calc(100cqi - 7rem) 0; } /* lane width minus chip and margins */
}
.chip--standard { animation-timing-function: var(--ease-standard); }
.chip--overshoot { animation-timing-function: var(--ease-overshoot); }
.chip--anticipate { animation-timing-function: var(--ease-anticipate); }
@media (prefers-reduced-motion: reduce) {
.chip { animation: none; }
}
</style>
</head>
<body>
<div class="lane"><span class="chip chip--standard">standard</span></div>
<div class="lane"><span class="chip chip--overshoot">overshoot</span></div>
<div class="lane"><span class="chip chip--anticipate">anticipate</span></div>
</body>
</html>
Naming the curves as tokens is the most important habit on this page. --ease-overshoot tells the next developer why the curve exists; cubic-bezier(0.34, 1.56, 0.64, 1) pasted into twelve rules tells them nothing and drifts into twelve slightly different values. Tokens also make it possible to tone the whole interface down later by editing one line.
The key technique: reversal swaps the effect
A subtle and useful property of Bezier easing: playing a curve backwards turns overshoot into anticipation. With animation-direction: alternate, or when a transition reverses because a hover ends, the browser plays the same curve in reverse time. An element that overshoots on arrival will therefore wind up — dip slightly past its starting position — before leaving.
For transitions this is usually what you want — a hover-lift that overshoots upward on enter will dip slightly on leave, which feels coherent. When it is not, give the two states different timing functions. The timing function declared on the state being entered governs that direction, so:
.card {
/* Leaving the hover state: calm, no wind-up. */
transition: translate 0.2s var(--ease-standard);
}
.card:hover,
.card:focus-visible {
translate: 0 -4px;
/* Entering the hover state: a small emphatic overshoot. */
transition: translate 0.35s var(--ease-overshoot);
}
@media (prefers-reduced-motion: reduce) {
.card, .card:hover, .card:focus-visible { transition: none; }
}
The asymmetry also follows a broader principle covered in Entry vs Exit Easing: arrivals can afford personality, departures should get out of the way quickly.
Tuning a curve by eye
Numbers alone rarely produce the right feel; tuning is visual. A reliable workflow:
- Start from a neutral ease-out that already works for the element, such as
cubic-bezier(0.22, 1, 0.36, 1). - Raise
y2in small steps — 1.1, 1.2, 1.3 — and replay the animation at real speed each time. Stop one step before the overshoot becomes the first thing you notice. The best overshoots are felt rather than seen. - Adjust
x2to move the peak. Smallerx2values put the overshoot earlier, which feels snappier; larger values delay it, which feels floatier. - Check at the largest real distance. If the element sometimes travels much further — a panel on a wide screen — replay at that distance and back the curve off if needed, or use a container-relative distance so the overshoot scales with it.
- Check with reduced motion enabled to confirm the fallback is a plain, quick transition rather than an abrupt jump.
Chromium and Firefox DevTools both include a Bezier editor: click the curve swatch next to any cubic-bezier() value in the Styles pane to drag control points and preview the result live. It is the fastest way to find a curve, after which the numbers go into a token.
Where character easing backfires
- Frequent, small interactions. Menu items, list hovers, form focus. Users trigger these dozens of times a minute; a bounce on each becomes tiresome.
- Large distances. A drawer that overshoots by 30 pixels looks broken. Use a gentler curve, or a spring from linear() tuned for the distance.
- Clamped properties.
opacity, colours andclip-pathinsets cannot usefully exceed their end value. Overshoot on them produces nothing visible except a sharper ease-out. - Layout properties. Overshooting
heightorwidthreflows the page twice per animation — once past the target, once back — and shifts surrounding content. Keep character easing ontranslate,scaleandrotate. - Error states. Anticipation reads as hesitation. Do not apply it to confirmation or error feedback, where immediacy matters.
Browser support
cubic-bezier() with y values outside 0–1 is supported in every browser that supports CSS transitions, which is universal: transitions date from Chrome 26, Edge 12, Firefox 16 and Safari 9. The standalone translate property used here is supported in Chrome and Edge 104+, Firefox 72+ and Safari 14.1+; transform: translate() behaves identically in older engines. prefers-reduced-motion is supported in Chrome 74+, Edge 79+, Firefox 63+ and Safari 10.1+.
FAQ
How do I make a cubic-bezier overshoot?
Give the second control point a y value greater than 1, as in cubic-bezier(0.34, 1.56, 0.64, 1). The curve then passes above the end value before returning to it, so an element travels slightly past its target and settles back.
What is anticipation in animation? A small movement in the opposite direction before the main movement, like a crouch before a jump. In CSS it comes from a first control point with a y value below 0, which makes progress dip negative at the start before accelerating toward the end.
Can the x values of cubic-bezier go outside 0 to 1?
No. The x coordinates represent time and must stay within 0 and 1, otherwise the declaration is invalid. Only the y coordinates, which represent progress, may go beyond that range.
Why does my overshoot look wrong on opacity?opacity is clamped to the range 0 to 1, so progress above 1 has no visible effect and the curve just looks like a sharp ease-out. Overshoot only reads on properties that can exceed their end value, such as translate, scale and rotate.
Related
- Easing & Motion Design — the parent guide.
- Spring and Bounce Easing With linear() — when one overshoot is not enough.
- Entry vs Exit Easing — pairing curves per direction.
- Smooth Hover Effects Without JavaScript — applying these curves to hover states.
- Motion That Scales With Container Size — keeping overshoot proportional to distance.
Related articles
More pages in the same section.