Chapter 02 of 11Part 1 · Principles
CSS easing functions and timing
How long UI motion should take, which curve to use for entering and leaving, and how to write your own easing.
10 minBy Zuzo StudioUpdated
In this chapter you will
- pick a duration for a movement based on its size and purpose
- choose between ease-out, ease-in and ease-in-out, and write a custom cubic-bezier()
- store your durations and curves as custom properties for the project
Before you startThis builds on UI animation principles: why motion?.
Two animations can move the same box the same distance in the same time and feel completely different. One is crisp, the other sluggish, a third slightly cheap. The difference is almost always duration and easing. CSS easing functions describe how a value changes over the length of an animation: fast at the start and slow at the end, the reverse, or something custom. This chapter covers how long UI motion should take, which curve to use for things entering and leaving, how to write your own with cubic-bezier(), and the two newer tools, linear() and steps(). You finish by putting your choices into custom properties that the rest of the course uses.
01Duration: how long UI motion should take
Duration is how long a movement takes from start to finish. For interface motion it is shorter than most people expect. web.dev's guide to choosing the right easing suggests around 200ms to 500ms for most eased movements, and warns that too short feels aggressive and too long feels obstructive.
Within that range, the size of the change matters. These are the ranges we start from, then adjust by eye:
| Movement | Starting range |
|---|---|
| Hover, press, focus feedback | 100ms to 200ms |
| Small things appearing: tooltips, dropdowns | 150ms to 250ms |
| Panels, dialogs, cards entering | 250ms to 400ms |
| Large moves across the screen | 400ms to 600ms |
| A full intro sequence, start to finish | under about 1s |
Three rules sit behind the table. Small and close moves are quick, large and far moves take a little longer. Things leaving can be faster than things arriving, because the reader no longer needs to follow them. And anything the reader triggers should start at once: a delay before feedback reads as lag, however smooth the motion after it.
02The keyword CSS easing functions
An easing function maps time to progress. At 0% of the duration the value is at its start, at 100% it is at its end, and the easing decides where it is at every point in between. A straight line means constant speed. A curve that rises steeply and then flattens means fast, then slow.
CSS has five keywords. The CSS Easing Functions specification defines each of the curved ones as a cubic Bézier curve:
| Keyword | Feels like | Equivalent |
|---|---|---|
linear |
Constant speed, mechanical | linear(0, 1) |
ease |
Quick start, gentle finish (the default) | cubic-bezier(0.25, 0.1, 0.25, 1) |
ease-in |
Slow start, sharp finish | cubic-bezier(0.42, 0, 1, 1) |
ease-out |
Quick start, soft landing | cubic-bezier(0, 0, 0.58, 1) |
ease-in-out |
Slow at both ends | cubic-bezier(0.42, 0, 0.58, 1) |
The demo below puts all five side by side. Press Play and five dots travel the same distance in the same time, one row per keyword. Drag the duration slider between 150ms and 1200ms to see how each curve changes character at different speeds. The sixth row has a custom cubic-bezier(): edit its four numbers and the demo shows the CSS it produces.
Linear motion moves at one speed. Ease out starts fast and settles, which suits things arriving on screen. Ease in starts slow and speeds up, which suits things leaving. Ease in out does both.
Watch where each dot is halfway through. The ease-out dot is already most of the way there. The ease-in dot has barely left. That is the whole difference, and it is why the choice matters more than the exact number of milliseconds.
Ease-out for entering, ease-in for leaving
The rule of thumb that covers most interface motion:
- Entering: ease-out. Something arriving should respond at once and settle gently, like a drawer that glides to a stop. web.dev calls ease-out "the right call, and certainly a good default" for this reason.
- Leaving: ease-in. Something going away can start slowly and accelerate out of view. The sharp finish of an ease-in would jolt if the reader watched it stop, but a leaving element does not stop on screen: it is gone or faded by then.
- Moving within the screen: ease-in-out. An element travelling from one place to another on screen, such as a slider handle or a tab indicator, starts and stops in view, so it eases at both ends.
linear has a place too: for things that loop continuously, like a spinner, and for anything tied to scroll position, where the reader's own movement supplies the feel. For a one off movement it looks robotic.
03Writing your own curve with cubic-bezier()
The keywords are a starting point. Most studio work uses custom curves, written with cubic-bezier(x1, y1, x2, y2). MDN's cubic-bezier() reference explains the model: the curve always starts at (0, 0) and ends at (1, 1), and the four numbers place two control points that bend it. The x values are time and must stay between 0 and 1, otherwise the whole declaration is ignored. The y values are progress and can go outside that range, which makes the value overshoot its target and come back, a small bounce.
.panel {
/* a stronger ease-out than the keyword */
transition: transform 300ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.badge {
/* y2 above 1: overshoots slightly, then settles */
transition: transform 250ms cubic-bezier(0.3, 1.4, 0.6, 1);
}
A practical way to read the numbers: a large y1 compared with x1 means a fast start. A small x2 with y2 at 1 means a long, soft landing. Stronger curves than the keywords tend to feel more modern, because more of the movement happens early and the rest is a slow settle.
04linear() and steps()
Two easing functions do things a single Bézier curve cannot.
linear() for springs and bounces
linear() takes a list of progress points and joins them with straight lines. With two points, linear(0, 1), it is the plain linear keyword. With many points it can trace any shape, including a real bounce or a spring that wobbles a few times before settling, which a cubic Bézier cannot draw. Each point can take a percentage that says when it is reached.
.toast {
transition: transform 600ms linear(
0, 0.6 20%, 1.08 40%, 0.97 60%, 1.01 80%, 1
);
}
Here the value passes its target at 40%, swings back a little and settles. You rarely write these by hand: spring generators output a linear() list for you. MDN marks linear() as widely available across browsers since December 2023, so it is safe for production. Keep springs for small, playful details; a dialog that wobbles on every open soon wears thin.
steps() for frame by frame motion
steps() does the opposite of smoothing. It divides the duration into equal jumps, so the value holds still and then snaps to the next position. MDN's steps() reference lists the options: steps(4, jump-end) jumps at the end of each interval, jump-start at the beginning, and if you leave the second value out it defaults to end.
.cursor {
/* a blinking text cursor: on, then off */
animation: blink 1s steps(2, jump-none) infinite;
}
Use it for sprite sheet animation, a typewriter effect, a ticking clock hand or a blinking cursor: anything that should look like separate frames rather than smooth movement. We build looping animations like that one in CSS keyframe animation.
05Easing and duration as tokens
Hard coded timings scattered through a stylesheet drift apart. One hover is 180ms, another 220ms, a third ease instead of the custom curve, and the site stops feeling like one thing. The fix is the same one the CSS cascade chapter of our first course uses for colours: name each value once as a custom property and refer to it everywhere. Three durations and three curves cover nearly every case on a marketing site.
Go back to your motion brief from why motion. If you wrote "calm, precise, quick", pick curves with no overshoot and durations near the short end of the table. If you wrote "playful", allow one curve with a little overshoot for small details.
:root {
--dur-fast: 150ms; /* hover, press, focus */
--dur-base: 250ms; /* small things entering */
--dur-slow: 400ms; /* larger moves, the hero */
--ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-in: cubic-bezier(0.5, 0, 0.75, 0);
--ease-in-out: cubic-bezier(0.6, 0, 0.3, 1);
}
Check your understanding
3 questions
Key takeaways
- Most interface motion sits between about 150ms and 500ms, with small and frequent movements at the short end.
- Use ease-out for things entering, ease-in for things leaving and ease-in-out for things moving within the screen.
- In cubic-bezier() the x values must stay between 0 and 1, while y values outside that range create an overshoot.
- linear() can trace springs and bounces with many points, and steps() makes motion jump in frames.
- Naming three durations and three curves as custom properties keeps a whole site's motion consistent.
Sources
- Choosing the right easingweb.dev, checked
- CSS Easing Functions Level 2W3C CSS Working Group, checked
- cubic-bezier()MDN Web Docs, checked
- linear()MDN Web Docs, checked
- steps()MDN Web Docs, checked