Spring Physics
The damped harmonic oscillator behind modern motion.
Duration-based easing answers “how should A travel to B over N milliseconds?”. A spring refuses the question. It has no timeline — it has physics. Give it a target and it accelerates, overshoots, settles, and (crucially) inherits its velocity if you interrupt it. That single property is why modern motion feels alive under your finger.
A spring is a damped harmonic oscillator
Every frame, two forces act on the object. Hooke's law pulls it toward the target proportional to how far away it is; a damping force opposes its velocity, bleeding off energy so it doesn't oscillate forever. Newton turns the net force into acceleration:
x = displacement from target · v = velocity · k stiffness · c damping · m massTo animate it we integrate per frame. We use semi-implicit (symplectic) Euler: update velocity from the current acceleration first, then move the position with that new velocity. It's one extra line versus naïve Euler and dramatically more stable for stiff springs:
const a = (-k*(x - target) - c*v) / m; // acceleration
v += a * dt; // velocity FIRST
x += v * dt; // then position The lab: tune the spring, watch ζ
This object is driven by a hand-rolled requestAnimationFrame integrator — the exact stepSpring() above, no library. Click or drag anywhere to set a
target. Drop damping for bounce, raise it past critical for sludge. The badge shows the live
damping ratio and classification.
The curve is computed by the same sampleSpring() integrator that drives the box.
Drag damping down to see overshoot/oscillation (ζ<1); raise it past critical
(ζ=1) for a slow, monotonic approach (ζ>1).
Velocity inheritance, isolated
Same idea, stripped down. Flick the object to give it momentum, then retarget while it's still moving. With inheritance on, it banks into the new target like a thrown ball; off, it forgets its motion and lurches. This is the property gestures and drag-release depend on.
Use the flick button in the lab above, then immediately click a new target. The same inherit velocity toggle governs both. There is no separate engine here —
velocity inheritance falls out for free from never resetting v.
The designer-friendly model: duration + bounce
Thinking in raw stiffness and damping is unintuitive. Apple's SwiftUI (and Motion's spring()) expose a perceptual reparameterisation: a perceived duration and a bounce from 0 to 1, converted to physics under the hood with
mass fixed at 1:
Baking a spring into CSS with linear()
The 2023 linear() easing function lets you approximate any curve — including a
spring — with a long list of sampled points. We sample the spring until it settles and emit each point as progress percent%. The result runs as pure CSS, even on the
compositor.
Widely available since 2024; the practical way to ship a 'spring' as plain CSS.
linear(0.0000 0.00%, 0.0205 2.13%, 0.0595 4.26%, 0.1132 6.38%, 0.1783 8.51%, 0.2518 10.64%, 0.3307 12.77%, 0.4126 14.89%, 0.4945 17.02%, 0.5746 19.15%, 0.6514 21.28%, 0.7238 23.40%, 0.7910 25.53%, 0.8523 27.66%, 0.9065 29.79%, 0.9536 31.91%, 0.9942 34.04%, 1.0284 36.17%, 1.0565 38.30%, 1.0788 40.43%, 1.0958 42.55%, 1.1071 44.68%, 1.1141 46.81%, 1.1175 48.94%, 1.1176 51.06%, 1.1151 53.19%, 1.1103 55.32%, 1.1037 57.45%, 1.0956 59.57%, 1.0867 61.70%, 1.0773 63.83%, 1.0676 65.96%, 1.0579 68.09%, 1.0485 70.21%, 1.0394 72.34%, 1.0310 74.47%, 1.0233 76.60%, 1.0163 78.72%, 1.0100 80.85%, 1.0045 82.98%, 0.9997 85.11%, 0.9959 87.23%, 0.9927 89.36%, 0.9903 91.49%, 0.9884 93.62%, 0.9872 95.74%, 0.9884 97.87%, 1.0000 100.00%) For an uninterrupted play-through, the baked CSS curve and the live JS spring track each other. Interrupt mid-flight, though, and only the JS spring inherits velocity — the baked curve restarts from a fixed easing.
Playground: a spring in fifteen lines
No imports, no framework — just the integrator animating a DOM box's transform.
Tune k, c, m at the top, click anywhere to retarget, and
note that velocity is never reset. Break it: set c to 2 and watch it ring; set it
to 40 and watch it ooze.