Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 9.5Intermediate ~21 min · shot 5 of 7

Springs in CSS with linear()

In one line

Sample a spring into a list of numbers and CSS linear() plays it as an easing, without any JavaScript during the movement. It cannot take an incoming speed.

0/7 · 0%

Think of it like this

A flip book. You cannot change its story once drawn, but anyone can play it anywhere with no artist in the room. linear() is the flip book of a spring: your script draws the pages once and the browser plays them.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

linear()
A CSS easing made of a list of values joined by straight lines.
stop
One value in the list. Stops are spaced evenly unless you give percentages.
sampling
Reading a curve at regular steps to make a list of numbers.
fallback
A simpler declaration placed first, kept by browsers that cannot read the second.

Step by step

01Look at the easing

Read the third console line: the first stops are 0, 0.07, 0.215, 0.379. The spring starts slowly, since it starts at rest.

02Compare with ease-out

Press Toggle, pause at 100 ms and read both tracks. Open the spring track's graph to see the overshoot past 1.

03Interrupt both

Press Toggle twice, 100 ms apart: both boxes start a new curve at the second click and neither keeps its speed.

04Cut the spring

In "A spring cut off" toggle both boxes and step the last frames: the lower one is at 148.8 px at 183 ms and at 160 at 200 ms, an 11 px last frame where the earlier ones moved about 6.

Try it yourself

  1. 1

    Slow the spring down

    In "A spring in the stylesheet" use duration * 2 in the springy.style.transition line. The same overshoot plays twice as slowly: the list is a shape and the duration scales it in time.

  2. 2

    Cut at 300 ms

    In "A spring cut off" change 200 to 300 in the springEasing(...) call. The last stop becomes 1.002, so the box ends 0.3 px past 160 and no jump is visible.

Code & diagrams

A spring in the stylesheet

`springEasing()` runs the spring and returns a `duration` and a `linear()` string. The same box with `ease-out` is the comparison.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

linear() supported: true
spring k 380, c 34: 24 stops, 481 ms
first stops: linear(0, 0.07, 0.215, 0.379, 0.532, 0.662, 0.761, 0.8…
A spring cut off

The same spring sampled for 200 ms only, so its last stop is 0.953.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

whole spring: 481 ms, last stop 1
cut at 200 ms: last stop 0.953, so the box ends 8 px short of 160 and then jumps
A fallback for old browserscss
.sheet {
  transition: transform 400ms ease-out; /* every browser */
  transition: transform 481ms linear(0, 0.23, 0.552, 0.786, 0.915, 0.974, 0.996, 1.002, 1.003, 1.002, 1.001, 1); /* ignored where linear() is unknown */
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to end at 1

Read the lower box of "A spring cut off": its list stops at 0.953.

terminal
$ Press Toggle and step the last frames
── what you'll see ──
cut at 200 ms: last stop 0.953, so the box ends 8 px short of 160 and then jumps

Myth vs fact

Myth

A linear() spring behaves like a real spring.

Fact

It is a recording of one. Interrupt it and it restarts from rest, because the browser has a list and no physics.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A linear() transition on transform or opacity can run off the main thread, so the spring plays smoothly even when the page is busy. Generate the stops at build time: the string is a constant for a given spring and belongs in tokens.css.

Remember this

  1. 1

    The problem this solves: a cubic Bézier can overshoot once (a control point above 1) but can never swing back and forth, and a JavaScript spring needs a script running every frame. linear() lets a stylesheet carry a spring: the browser draws straight lines between the values you give it.

  2. 2

    How it is made. Run the spring in small steps, keep about two dozen positions, write them as linear(0, 0.07, 0.215, …, 1): the stops are evenly spaced in time. In the first example k 380 and c 34 gave 24 stops over 481 ms (the spring run until rest, thresholds 0.001 and 0.01). A different transition time stretches the same shape.

  3. 3

    More stops, smoother curve. Between stops the browser draws straight lines: too few show as a polygon, too many grow the file. Josh Comeau's article compares 11 and 50 stops. About two dozen suits UI distances.

  4. 4

    A spring cut off too soon. If you sample only part of the motion the last stop is not 1. The transition ends on that value, then drops the effect and shows the target: the element jumps. In the second example a spring cut at 200 ms ends at 0.953, 8 px short of 160, then jumps. End the list at 1.

  5. 5

    What you lose. The transition has no memory of its speed: reverse it and it plays a new copy of the spring from rest (9.4). It needs a duration chosen in advance and cannot follow a moving target.

  6. 6

    Support. linear() has worked in all major browsers since December 2023 (MDN marks it Baseline: widely available; first versions Chrome 113, Firefox 112, Safari 17.2). Older browsers drop the declaration, so write a normal easing first and the linear() one after it. For reduced motion, a springy move is still movement: inside @media (prefers-reduced-motion: reduce) swap it for an opacity fade (Topic 7.2).

Explain it without notes

01

How does a spring become a CSS easing, and what do you lose?

02

What happens if the last stop of a linear() spring is 0.9?

Practice

01

Write a box that uses a spring easing and falls back to ease-out in an old browser.

02

Why does linear() suit a hover effect but not a drag?

Done when you can

  • You can sample a spring into a linear() list

  • You can write a fallback for old browsers

  • You can say what interruption does to a linear() spring