Design FIELD NOTES · 4 MIN READ
CSS Transitions Explained: Timing, Easing and Delay
How the transition shorthand works: properties, duration, timing functions from ease to steps, and why transitions need a state change to fire.
A transition is the browser turning a sudden change into a smooth one. Change a button's background on hover and, without a transition, the color snaps; with transition: background-color 200ms, the browser fills in every intermediate frame. The whole feature lives in one property with four ingredients — and getting those four right is the difference between UI that feels fluid and UI that feels laggy or twitchy.
One property, four ingredients
The shorthand reads: transition: property duration timing-function delay. Only the duration is mandatory — the property defaults to all, the timing function to ease, and the delay to 0s. So transition: 300ms is legal and means "animate every animatable property over 300 milliseconds with the default curve".
Multiple comma-separated groups give different properties different timing, and the order matters within each group:
transition: opacity 200ms ease-out, transform 400ms cubic-bezier(0.2, 0.8, 0.2, 1) 100ms;
Here the fade finishes fast; the movement starts 100ms later and takes longer. A common mistake is listing a property without a duration — transition: opacity, transform 300ms is valid syntax, but the durationless opacity group is treated as 0ms and snaps instantly. Every property needs its own duration.
Timing functions: the shape of the motion
The timing function is a curve that maps time (x-axis, 0 to 1) to progress (y-axis). The keyword set covers the everyday choices: linear for constant speed, ease as the browser's gentle default, ease-in starting slow, ease-out ending slow, and ease-in-out for both. Beneath the keywords, everything is a cubic Bézier: ease is literally cubic-bezier(0.25, 0.1, 0.25, 1).
The Bézier form is where expressive motion lives. The two x values (control points on the time axis) must stay between 0 and 1 — you cannot travel backward in time. The y values (progress axis) may go outside that range, and that is how overshoot is made: cubic-bezier(0.34, 1.56, 0.64, 1) briefly passes 100% progress before settling, which reads as a springy landing. Material-style entrances use curves like cubic-bezier(0.4, 0, 0.2, 1) — accelerate hard, decelerate gently.
steps(): motion in slices
Not every animation should be smooth. steps(n, jump-end) divides the change into n discrete frames, holding each value before jumping to the next. This is the trick behind sprite-sheet characters (step through one row of frames), typewriter reveals (step a width through per-character jumps), and retro tickers. The jump position decides where within each slice the jump happens: jump-start changes immediately, jump-end at the end of each slice, jump-none and jump-both handle the edge cases. The legacy shorthand keywords step-start and step-end are steps(1) with those positions.
Transitions need a state change
A transition fires when a property's computed value changes after the element is rendered — via :hover, :focus, a class toggle, or a style mutation. It does not fire on initial page load: if the transition is declared in the same rule that sets the starting values, the browser renders once with everything in place and no change ever occurs. Related trap: declaring the transition only inside :hover means the enter animation runs but the exit snaps, because when the pointer leaves, the :hover rule (and its transition) no longer applies. Put the transition on the base rule so both directions animate.
When the choreography gets complex — loops, delays between keyframes, holding mid-values — that is @keyframes territory. The division of labor: transitions react to state changes; animations play on their own schedule.
Transition cheap things
Every transitioned property that affects layout — width, height, top, padding — makes the browser recompute layout and repaint for every frame of the animation. The two properties that skip all of that are transform and opacity, which the compositor can animate without touching layout at all. A "grow on hover" card is smoother implemented as transform: scale(1.03) than as a width change. Durations have conventions too: 150–250ms for hover feedback, 300–500ms for panels and modals. Anything past about a second starts to feel sluggish for UI feedback.
Try it yourself
The CSS Transition Generator builds the shorthand field by field — properties, duration, keywords, custom cubic-bezier curves and steps — and includes a demo box you can animate to feel each easing curve. Pair it with the CSS Filter Generator to see transitions animate filter effects, and the Font Size Clamp Generator when the text itself, not just its container, should respond smoothly to the viewport.