Explain animation timing-function values and what a custom cubic-bezier() curve does
Advertisement
🧩 Scenario
Architecture Walkthrough
What a Timing Function Actually Maps
A timing function does not change how long an animation takes. It maps elapsed time to progress through the value change.
Picture a graph with time from 0 to 1 on the X axis and progress from 0 to 1 on the Y axis. A straight diagonal line means progress advances at a constant rate: linear. A curve that rises steeply at first and flattens means most of the change happens early and the end is a gentle settle: that is ease-out. A curve that starts flat and rises steeply means a slow start and a fast finish: ease-in.
The duration is unchanged in all three cases. Only the distribution of the change within that duration differs.
The Keywords and What They Expand To
Every keyword except linear and steps() is a named cubic-bezier():
linear→ constant rate;cubic-bezier(0, 0, 1, 1)ease→cubic-bezier(0.25, 0.1, 0.25, 1), the default; a gentle acceleration and a longer decelerationease-in→cubic-bezier(0.42, 0, 1, 1); slow start, fast finishease-out→cubic-bezier(0, 0, 0.58, 1); fast start, slow finishease-in-out→cubic-bezier(0.42, 0, 0.58, 1); slow at both ends
Note that ease is the default for both transition and animation when no timing function is given, and it is not the same as ease-in-out. Its asymmetry, a short acceleration and a long deceleration, is why it feels acceptable for most things without being ideal for any.
Reading cubic-bezier()
cubic-bezier(x1, y1, x2, y2) gives the coordinates of two control points. The curve always starts at (0, 0) and ends at (1, 1); the two control points bend the path between them.
The X values must be between 0 and 1, because time cannot run backwards or extend beyond the duration. Values outside that range make the function invalid and the declaration is dropped.
The Y values may be outside 0 to 1, and that is what produces overshoot. A y2 greater than 1 means progress exceeds 100% before returning, so an element travels past its target and settles back, which is the basis of every "bounce" or "spring" easing. A negative y1 means progress briefly goes backwards, giving an anticipation or wind-up effect before the main motion.
Reading a curve then reduces to two questions: how steep is it at the start, and how steep at the end? Steep at the start means fast initial movement; steep at the end means it arrives abruptly.
Choosing Easing
The conventions are stable enough to treat as defaults.
Entrances use ease-out. Something arriving on screen should cover most of the distance quickly and then settle, which reads as responsive. ease-in on an entrance produces a slow start that feels like lag, and since the beginning of a transition is when the user is waiting for feedback, that is the worst place to be slow.
Exits use ease-in. Something leaving should start gently and accelerate away, which reads as decisive and lets the user stop watching sooner.
Moves and morphs use ease-in-out. An element travelling from one position to another with both endpoints on screen benefits from easing at both ends.
Continuous rotation uses linear. A spinner easing each revolution would visibly pulse. Linear is also correct for colour and opacity crossfades, where the eye is not tracking motion.
Overshoot for playful emphasis. A curve such as cubic-bezier(0.34, 1.56, 0.64, 1) gives a subtle spring, which suits a success state or a toggle but becomes tiresome on frequently repeated interactions.
Duration matters alongside easing: 150 to 250ms suits small UI feedback, 250 to 400ms suits panels and modals, and anything over roughly 500ms starts to feel slow for interaction feedback regardless of the curve.
steps() Is Not a Curve
steps(n, position) jumps between n discrete values rather than interpolating smoothly. steps(4, end) holds each of four values for a quarter of the duration.
Its main uses are sprite-sheet animation, where each step shows one frame of a strip, and deliberately mechanical effects such as a typewriter reveal or a stepped counter. step-start and step-end are shorthands for steps(1, start) and steps(1, end).
The linear() function is the newer addition worth knowing: it accepts a list of progress stops, which allows multi-segment and approximate spring curves that a single cubic Bézier cannot express.
Per-Keyframe Timing
In an animation, animation-timing-function applies between each pair of keyframes, not once across the whole sequence. A four-keyframe animation with ease-in-out eases into and out of every segment, which produces three visible slowdowns rather than one smooth arc. Setting animation-timing-function inside an individual keyframe overrides it for the segment starting at that keyframe, which is how per-segment easing is expressed.
Key Code Explained
/* The timing function distributes progress; it does NOT change duration.
All four of these take exactly 300ms. */
.linear { transition: transform 300ms linear; }
.ease-in { transition: transform 300ms ease-in; }
.ease-out { transition: transform 300ms ease-out; }
.ease-in-out { transition: transform 300ms ease-in-out; }
/* Keyword equivalents */
/* linear = cubic-bezier(0, 0, 1, 1) */
/* ease = cubic-bezier(0.25, 0.1, 0.25, 1) <- the DEFAULT */
/* ease-in = cubic-bezier(0.42, 0, 1, 1) */
/* ease-out = cubic-bezier(0, 0, 0.58, 1) */
/* ease-in-out = cubic-bezier(0.42, 0, 0.58, 1) */
/* cubic-bezier(x1, y1, x2, y2): X must be 0..1, Y may exceed it */
.overshoot {
/* y2 = 1.56 > 1 -> progress passes 100% and settles back */
transition: transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.anticipate {
/* y1 = -0.5 < 0 -> progress goes backwards first: a wind-up */
transition: transform 500ms cubic-bezier(0.5, -0.5, 0.5, 1.5);
}
.invalid {
/* X outside 0..1 is INVALID and the declaration is dropped */
transition: transform 300ms cubic-bezier(1.5, 0, 0.5, 1);
}
/* Conventions: ease-out to arrive, ease-in to leave */
.toast {
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
.toast.is-leaving {
transition: opacity 150ms ease-in, transform 150ms ease-in;
}
/* linear is CORRECT for continuous rotation */
.spinner {
animation: spin 1s linear infinite;
}
/* steps(): discrete jumps, not a curve */
.sprite {
animation: play 800ms steps(8, end) infinite; /* 8-frame sprite strip */
}
.typewriter {
animation: type 2s steps(24, end) forwards;
}
/* linear() expresses multi-segment curves a single Bezier cannot */
.springy {
transition: transform 600ms linear(0, 0.6 30%, 1.1 60%, 0.95 80%, 1);
}
/* In animations, easing applies BETWEEN EACH keyframe pair */
@keyframes bounce-path {
0% { transform: translateY(0); }
50% { transform: translateY(-40px); animation-timing-function: ease-in; }
100% { transform: translateY(0); }
}
.ball {
animation: bounce-path 600ms ease-out infinite;
/* ease-out applies 0%->50%; the inner declaration makes 50%->100% ease-in */
}
/* Reduced motion still applies regardless of the curve */
@media (prefers-reduced-motion: reduce) {
.toast, .springy { transition-duration: 0.01ms; }
}
The .overshoot and .anticipate rules are the ones that make cubic-bezier() worth understanding rather than copying from a generator. Both rely on Y values leaving the 0 to 1 range, and knowing that Y is unconstrained while X is not tells you immediately which effects are expressible and which are invalid. The .invalid rule is the counterpart: an X value above 1 is not a stronger curve, it is a dropped declaration.
The @keyframes bounce-path block is the detail that catches people out in animations. ease-out on the animation shorthand is applied to every segment, not across the whole sequence, so a ball rising and falling with one keyword eases at the midpoint too. Declaring animation-timing-function inside a keyframe is how you get a genuine arc.
Tradeoffs
| Timing function | Motion character | Best for |
|---|---|---|
linear | Constant rate | Spinners, crossfades, progress bars |
ease (default) | Short acceleration, long settle | General-purpose default |
ease-in | Slow start, fast finish | Exits, dismissals |
ease-out | Fast start, gentle settle | Entrances, most UI feedback |
ease-in-out | Slow at both ends | On-screen moves and morphs |
cubic-bezier() with y > 1 | Overshoot and settle | Playful emphasis, success states |
cubic-bezier() with y < 0 | Anticipation wind-up | Attention-drawing motion |
steps(n) | Discrete jumps | Sprite sheets, typewriter, counters |
linear(...) | Multi-segment | Approximate springs |
What Interviewers Actually Check
- Whether you know the timing function distributes progress rather than affecting duration
- Whether you know the four
cubic-bezier()numbers are two control points, and which axis is constrained - Whether you can explain overshoot in terms of a Y value exceeding 1
- Whether you apply the entrance and exit conventions correctly
- Whether you know easing applies per keyframe pair in an animation
Follow-Up Questions
- Why must the X control values stay within 0 to 1 while the Y values need not?
- How does
linear()express curves that a single cubic Bézier cannot, and what is a practical use? - What is the difference between
steps(4, start)andsteps(4, end), and which suits a sprite sheet? - Why does
easediffer fromease-in-out, and what does its asymmetry accomplish? - How do spring physics in JavaScript animation libraries differ from what any cubic Bézier can express?
Common Candidate Mistakes
- Believing the timing function changes how long an animation takes, when it only redistributes progress within a fixed duration
- Using
ease-infor an entrance, which delays the first visible movement at exactly the moment the user is waiting for feedback - Using
linearfor movement, which reads as mechanical because nothing physical accelerates instantly from a standing start - Assuming X control values outside 0 to 1 produce a stronger curve, when they make the function invalid and the declaration is dropped entirely
- Treating
steps()as a kind of easing curve, when it produces discrete jumps with no interpolation between them
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain what the two axes of the timing curve represent?
- Can you expand
ease,ease-in,ease-out, andease-in-outinto theircubic-bezier()equivalents? - Can you explain how a Y value above 1 produces overshoot and why X cannot exceed 1?
- Can you choose the right easing for an entrance, an exit, an on-screen move, and a spinner?
- Can you explain what
steps()does and name a real use for it?
Summary
A timing function maps elapsed time to progress through a value change. It never alters duration; it redistributes the change within that duration. On a graph of time against progress, a diagonal line is linear, a curve steep at the start is ease-out, and a curve steep at the end is ease-in. All keywords except linear and steps() are named cubic-bezier() values, and the default is ease, which is asymmetric and not the same as ease-in-out.
cubic-bezier(x1, y1, x2, y2) supplies two control points bending a path from (0, 0) to (1, 1). The X values must stay within 0 to 1, since time cannot reverse or exceed the duration, and an out-of-range X invalidates the declaration. The Y values are unconstrained, which is exactly what enables overshoot: a y2 above 1 carries progress past 100% so the element travels beyond its target and settles back, while a negative y1 produces an anticipation wind-up.
The conventions are worth treating as defaults. Entrances use ease-out so the motion arrives quickly and settles, exits use ease-in so they leave decisively, on-screen moves use ease-in-out, and continuous rotation uses linear so a spinner does not pulse. steps() produces discrete jumps for sprite sheets and typewriter effects rather than any kind of curve, and linear() covers multi-segment and approximate spring curves a single Bézier cannot express. In animations, remember that the timing function applies between each keyframe pair rather than across the whole sequence, so per-segment easing is declared inside individual keyframes.
What do the four numbers in cubic-bezier() mean?
They are the x and y coordinates of two control points: x1, y1, x2, y2. The start point is fixed at 0,0 and the end at 1,1. X values must stay between 0 and 1; Y values may go outside it to overshoot.
Why is linear rarely the right choice?
Because nothing physical moves at a constant speed from a standing start. Linear reads as mechanical for movement, though it is correct for continuous rotation such as a spinner and for colour or opacity crossfades.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement