Springs
intermediate·8 min·Markdown for agents
What this lesson teaches
Know when a spring beats a curve, tune stiffness and damping by feel, and keep hand-written motion landing in the same place at any frame rate.
Send the ball right, then press again before it arrives. It turns around without stopping dead, carrying its speed into the new direction. That is a spring.
{ type: "spring", stiffness: 380, damping: 30, mass: 1 }Press the button twice quickly. The ball turns around with its speed intact instead of stopping dead: that is the reason to reach for a spring.
A curve says "get there in 200ms, with this shape". A says "pull toward the target with this much force and this much friction", and the duration falls out of the physics. That difference only matters in one situation, but it matters a lot there: when the person is steering the motion, use a spring.
When springs win, and when they don't
Use a spring when:
- The motion can be interrupted halfway: a drag, a swipe, a sheet you grab again.
- It starts from a real speed, like the flick of a finger.
- The target can change while it's moving.
Use a curve when a discrete event triggers it (a click, a route change), when you need exact timing so several things land together, or when the element is small enough that nobody could tell. A two-state press doesn't need a spring either: a CSS transition already reverses from wherever it is.
The two springs to remember
Apple's default is critically damped: no bounce, a perceived duration around 0.3–0.4 seconds. In Motion you can write it in the form that's easiest to reason about:
const settle = { type: "spring", duration: 0.35, bounce: 0 };The drawer spring settles a sheet or drawer after you let go of it:
const drawer = { type: "spring", stiffness: 380, damping: 30, mass: 1 };Bounce is earned. A card you flicked may overshoot a touch (bounce 0.1 to 0.3, never more); a menu that simply opened should not. And never leave a spring on library defaults: they tend to be slow and floaty (try "Library default" above). Always set duration and bounce, or both stiffness and damping.
Not everything polished is a spring, either. Sonner, the usual example of polished motion, uses plain CSS transitions.
Writing your own: frame-rate independence
Libraries handle the physics for you. If you write a spring or a decay by hand, there's a trap. Flick both balls below in each mode.
Code written "per frame" (velocity *= 0.95 every frame) runs twice as many frames on a 60Hz screen as on a 30Hz one, and the same gesture ends somewhere different. Cheng Lou measured the same input landing up to 359px apart between 10 and 120fps. The rules from his work, in plain words:
Step on a fixed clock
Advance springs in fixed 4ms steps (four per frame at 60fps), or use a formula that gives the exact value at any time, likex0 · e^(−kt).Cap the catch-up
After the tab freezes, don't run thousands of steps at once. Cap at about 300 (1.2s), then either carry the rest over or snap to the destination, on purpose.Apply input at its timestamp
Step up to the moment of the press, then apply it, so recorded input replays identically.Store what the person placed
Keep the panel size they chose; compute the squeezed-to-fit version each frame without saving it.Never decide from last frame's value
A lagging position is fine to draw; a lagging width that picks a column count makes things jump.
Ending a spring needs care too: it never reaches the target exactly. Stop it when both the distance and the speed are tiny (under about 0.01), then set it exactly on target. The playground above does all of this, which is why interrupting it feels clean.
The ball above is stepped every 4ms no matter how fast your screen is.Live data: breathe as one
A cousin of the spring comes from Benji Taylor's live chart, Liveline. When several things show the same changing number (the line, the price badge, the axis labels), move them all toward the new value at the same rate, so the chart inhales as one instead of jittering in pieces. He uses a of 8% per frame:
current += (target - current) * 0.08; // for every element that shows this value, in one loopIf a new value lands outside the visible range, snap the range out instantly, then let the line glide into it. Run everything in one requestAnimationFrame loop that writes to the page directly rather than through React state. And give live data its states: a loading line that breathes and then becomes the chart, a quiet empty message, a paused view that catches up on resume. Test it on ugly data: wild swings, fast reversals, long silences then bursts.
Use springs only where the user drives the motion (drag, swipe, sheets, anything interruptible); keep curves for click-triggered animations. Default to a critically damped spring ({ type: "spring", duration: 0.35, bounce: 0 }); for drawers and sheets after a drag use { stiffness: 380, damping: 30 } and pass the release velocity. Only allow bounce (0.1–0.3) after a flick. Never leave spring parameters on defaults. If you hand-write any physics, step it on a fixed 4ms clock with a capped catch-up, not once per frame.Quick check
Which of these should use a spring rather than a cubic-bezier curve?
In the skill
The nodes your agent reads when it does this work.
- Spring animationsWhen springs beat curves — interactive/draggable elements. Apple's critically damped default (bounce 0, ~0.35s), the 380/30 drawer spring, bounce only after momentum. Interruptibility.
motion/spring-animations - Frame-rate independenceAnimation state should depend on time and events, never on which frames happened to run. Step springs on a fixed clock or sample a closed form, cap the catch-up after a stall, apply events at their timestamps, store what the user placed and derive what fits, and never let last frame's value make a decision.
motion/frame-rate-independence - Lerp breathing — one update, many elementsUpdate multiple visual elements with the same lerp rate so they breathe as one organism, not as desynced parts.
motion/lerp-breathing
Connected lessons
What this lesson builds on and leads to, across every course.