WebCraft

linear()

A spring in the easing function

ease-out
linear() spring

CaveatBoth knobs leave together and both take 550ms; only the lower one arrives like an object with mass. linear() is not a curve — it is a polyline the engine walks between the stops you hand it, so a spring is a real spring equation sampled into two dozen of them. That is the first thing it costs: nobody writes this by hand, and a stop count low enough to read is low enough to see, as facets in what should be smooth. The second is that it is a timing function over a fixed duration and not physics, so it has no state. Toggle twice quickly and it does not carry its velocity into the new run the way a spring would; it restarts along the curve from wherever it stood, and the overshoot lands somewhere the eye reads as a fault. And the overshoot only exists on properties that can pass their end value: translate and transform can, opacity clamps at 1, so a bounce written on a fade is quietly just a fade.

FallbackWidely available since 2024; where it is not understood the value is dropped and the easing falls back to plain ease — the motion still happens, without the character. The real trap is the shorthand: written as transition: translate .55s linear(…), a rejected value takes the whole declaration with it, so the transition disappears rather than degrading. Where that matters, keep the shorthand for property and duration and set transition-timing-function on a line of its own. And it belongs inside prefers-reduced-motion: an overshoot is precisely the thing people switch motion off to be rid of.

HTML + CSS
<div class="lane"><i class="knob"></i></div>

/* generated from mass, stiffness and
   damping — not typed by hand */
.knob {
  transition: translate .55s linear(
    0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%,
    0.723 12.9%, 0.938 16.7%, 1.017, 1.077,
    1.121, 1.149 24.3%, 1.159, 1.163, 1.161,
    1.154 29.9%, 1.129 32.8%, 1.051 39.6%,
    1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%,
    0.975 57.1%, 0.997 69.8%, 1.003 76.9%, 1);
}

/* values over 1 are the overshoot */
.demo:has(input:checked) .knob { translate: 185px; }

Published

Questions

<details name>

How do I make a spring animation in CSS without JavaScript?

Sample a spring equation into stops and hand them to the linear() easing function — values above 1 are the overshoot. Generate the list; a stop count small enough to write by hand is small enough to see as facets in the motion.

Why does my linear() easing look jerky?

linear() interpolates in straight segments between the stops, so too few stops leave visible corners in what should be a smooth curve. More stops cost nothing at runtime and are unreadable by design.

Why does the bounce not show up on opacity?

Because the overshoot needs a property that can pass its end value. translate and transform can; opacity clamps at 1, so everything above 1 in the curve renders identically and the bounce is silently just a fade.