Shots in this scene
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.
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
Slow the spring down
In "A spring in the stylesheet" use
duration * 2in thespringy.style.transitionline. The same overshoot plays twice as slowly: the list is a shape and the duration scales it in time. - 2
Cut at 300 ms
In "A spring cut off" change
200to300in thespringEasing(...)call. The last stop becomes 1.002, so the box ends 0.3 px past 160 and no jump is visible.
Code & diagrams
`springEasing()` runs the spring and returns a `duration` and a `linear()` string. The same box with `ease-out` is the comparison.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
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…The same spring sampled for 200 ms only, so its last stop is 0.953.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
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.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.
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 ontransformoropacitycan 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 intokens.css.
Remember this
- 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
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 differenttransitiontime stretches the same shape. - 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
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
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
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 thelinear()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
How does a spring become a CSS easing, and what do you lose?
What happens if the last stop of a linear() spring is 0.9?
Practice
Write a box that uses a spring easing and falls back to ease-out in an old browser.
Why does linear() suit a hover effect but not a drag?
Done when you can
You can sample a spring into a
linear()listYou can write a fallback for old browsers
You can say what interruption does to a
linear()spring