// blog/developer/
Back to Blog
Developer · Published June 13, 2026 · 8 min read · By Toine ·

Update note: Rewritten from experience; two links to tools that do not exist removed and the CSS animation generator linked instead, the CSS spring() claim corrected (proposed, not shipping)

CSS Timing Functions: Ease-Out for Nearly Everything, and Keep It Short

CSS Timing Functions: Ease-Out for Nearly Everything, and Keep It Short

Two animations can move the same element the same distance in the same number of milliseconds, and one looks right while the other looks like a spreadsheet moving a cell. The difference is the timing function, the curve that says how the speed changes over the duration. Nothing physical moves at a constant speed; things start, pick up, and settle. An interface that ignores that looks wrong to people who could not tell you why.

My own position on interface motion is a tester's position: most of it is too long, and every animation is a wait. A 400 millisecond modal is 400 milliseconds on every test step that opens it, thousands of times a release, and it is the same wait for a user who opens it fifty times a day. On ToolForte, which I built and maintain alone, almost all the motion is colour fades of 150 to 200 milliseconds and nothing slides anywhere. So this post is the curves and when to use them, the durations I would defend, and the properties that make animation stutter.

* * *

Five keywords, and ease-out wins most of the time

CSS ships five named curves, and they cover most of what an interface needs.

linear is constant speed. It reads as mechanical, which is right for a progress bar or a spinner and wrong for anything that is supposed to feel like it arrived. ease is the default when you specify nothing: a slow start, a fast middle, a slow end. It is fine, and slightly sluggish off the mark for something the user just clicked. ease-in starts slowly and accelerates, which suggests the element is leaving and gathering speed as it goes; use it for exits. ease-out starts fast and slows to a stop, which is what an arriving element should do, and what a click should get as feedback: instant movement, settled finish. ease-in-out is symmetric, for something that stays on screen and moves from one place to another.

If you set one rule for a codebase, make it ease-out on entrances and interactions, ease-in on exits, and nothing else without a reason. Most of the motion I see that feels heavy is ease or ease-in-out on a hover state, where the slow start reads as lag.

Smooth motion blur abstract representing animation curves
Smooth motion blur abstract representing animation curves
* * *

Custom curves worth copying

When the keywords are not enough, cubic-bezier takes four numbers: two control points on a graph where the horizontal axis is time from 0 to 1 and the vertical axis is progress from 0 to 1.

`css transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1.0); `

The form is cubic-bezier(x1, y1, x2, y2). The first point shapes the start, the second the end. A handful of values I would keep in a snippet file:

A firm ease-out, Material's decelerate curve, for elements arriving: `css cubic-bezier(0.0, 0.0, 0.2, 1.0) `

Material's standard curve, for elements moving on screen: `css cubic-bezier(0.4, 0.0, 0.2, 1.0) `

A cubic ease-out with a longer settle, the easeOutCubic from easings.net: `css cubic-bezier(0.215, 0.61, 0.355, 1.0) `

A soft overshoot, easeOutBack, where the element passes its target and comes back: `css cubic-bezier(0.34, 1.56, 0.64, 1.0) `

The vertical values may go outside 0 to 1, which is how the overshoot works: progress briefly exceeds one hundred percent and returns. It reads as springy. Use it on one thing per screen at most; on everything it reads as a toy.

Key takeaway

When the keywords are not enough, `cubic-bezier` takes four numbers: two control points on a graph where the horizontal axis is time from 0 to 1 and the vertical axis is progress from 0 to 1.

* * *

Duration: shorter than you think

The curve sets the shape; the duration sets whether anyone waits. The ranges the large design systems agree on are close to what I would choose myself.

Under about 100 milliseconds for micro-interactions: button states, hover, a toggle. Long enough to be seen, short enough to be felt as immediate. Between 150 and 300 for ordinary transitions: a menu opening, a modal appearing, a sidebar expanding. Up to 500 for large movements where the eye has to travel, a page transition or a big element changing place. Beyond that is decoration, and nothing that responds to a click should ever be there.

Scale duration with distance. A tooltip that moves ten pixels wants about 100 milliseconds; a sidebar sliding three hundred pixels wants 250 to 300; a full-viewport slide wants 400 to 500. The failure I see most is the reverse: a small element given a long duration because it looked nice in the design tool, at a scale where the designer watched it once. Users watch it hundreds of times.

Spinners are the exception to all of it: linear, infinite, and no easing, because a spinner is a signal that something is still happening, not a movement to a destination.

* * *

Stagger a list, then cap it

When several elements enter at once, a short delay between them reads as intentional rather than as a page loading in a lump. The usual pattern is a cascade on a list:

`css .item { opacity: 0; transform: translateY(20px); animation: fadeUp 300ms ease-out forwards; } .item:nth-child(1) { animation-delay: 0ms; } .item:nth-child(2) { animation-delay: 50ms; } .item:nth-child(3) { animation-delay: 100ms; } .item:nth-child(4) { animation-delay: 150ms; }

@keyframes fadeUp { to { opacity: 1; transform: translateY(0); } } `

Thirty to eighty milliseconds between items keeps the sequence connected while each entrance is still visible on its own. Cap the stagger at the first five to eight items. Past that the total runs long and the later items sit invisible for a second or more, which is the one outcome worse than no animation: content that exists and cannot be seen. Items beyond the cap should appear at once, or at a fixed maximum delay.

The CSS Animation Generator writes the keyframes and the easing for a pattern like this so you can adjust the numbers and copy the result rather than typing the boilerplate.

Developer working on web animations on a monitor
Developer working on web animations on a monitor
* * *

Animate transform and opacity, and almost nothing else

A smooth animation renders a frame every 16.7 milliseconds at 60 frames per second, and faster still on the 120 hertz screens most phones now have. If a frame triggers layout or paint work that takes longer than its slot, frames drop and the motion stutters.

Two properties are safe: transform (translate, rotate, scale, skew) and opacity. The browser's compositor handles them off the main thread, so they stay smooth even while JavaScript is busy. filter is usually composited too, but not everywhere, so I would not rely on it.

Properties that cause layout, and should not be animated, are width, height, padding, margin, top, left and their siblings, font-size and border-width. Properties that cause paint, cheaper but not free, are background-color, color, box-shadow and border-color. The practical rewrite is mechanical: left: 0 to left: 100px becomes transform: translateX(100px); width: 0 to width: 200px becomes transform: scaleX(1) with a transform-origin.

will-change tells the browser an element is about to move so it can prepare a layer:

`css .about-to-animate { will-change: transform, opacity; } `

Use it on the element that is about to animate, add it just before and remove it after. On many elements at once it costs graphics memory and makes things slower, which is the opposite of what people apply it for.

When the keyframes have grown, run the stylesheet through the CSS Minifier before shipping; keyframe blocks with many steps are mostly whitespace.

* * *

What linear() adds, and where springs stand

A cubic-bezier has two control points, which means it cannot bounce more than once or describe anything with several turns. The linear() function, supported in every current browser, takes as many points as you like and interpolates straight lines between them:

`css transition-timing-function: linear( 0, 0.006, 0.025, 0.056, 0.1, 0.157, 0.225, 0.306, 0.4, 0.506, 0.625, 0.756, 0.9, 1.056, 1.125, 1.106, 1.05, 1.006, 0.981, 0.975, 0.981, 1 ); `

That list is a sampled spring bounce, the kind of curve that previously needed a JavaScript library. Tools exist that sample a spring and emit the linear() for it.

A true spring is a simulation, mass, stiffness and damping, rather than a fixed curve, and its practical advantage is interruption. If a user triggers a new animation while the old one is running, a spring carries the current velocity into the new motion; a bezier restarts from zero and jumps. Framer Motion and React Spring give you that in JavaScript. A CSS spring() easing has been proposed and has not shipped anywhere as of this update, so do not plan on it.

For most production CSS the keywords and a couple of bezier values cover the need. I would learn linear() for the one or two places where a bounce is the point, and leave springs to the JavaScript layer until there is a native one.

Key takeaway

A cubic-bezier has two control points, which means it cannot bounce more than once or describe anything with several turns.

* * *

FAQ

Which timing function should be the default?

ease-out. It responds instantly to the click and settles at the end. The built-in ease has a slower start that reads as lag on interactive elements.

Why do animations feel slow on phones?

Partly expectation: people expect a phone to respond faster than a desktop, so I would trim durations by a fifth to a third on small screens. Partly hardware: mobile graphics are more sensitive to paint work, so the transform-and-opacity rule matters more there, not less.

Should I honour prefers-reduced-motion?

Yes, and not as an afterthought. Remove decorative motion entirely and replace functional motion, a sidebar opening, a modal appearing, with an instant change or a short opacity fade. The blanket approach is crude and works:

`css @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } `

Can CSS animation hold 60 frames per second?

Yes, if it animates only transform and opacity, avoids layout properties, uses will-change sparingly, and is tested on a slow device rather than on the developer's machine. The compositor runs those two properties independently of the main thread, so even heavy script does not make them stutter.

Try these tools
· 🔧 Css Minifier