Skip to content
ExamsLibraryFor agentsConnect your agent

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.

  1. 1When springs win, and when they don't
  2. 2The two springs to remember
  3. 3Writing your own: frame-rate independence
  4. 4Live data: breathe as one

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.

A whisper of overshoot · settles in 408ms
target2s{ 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.

Tune the spring, then interrupt it mid-flight.

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.

60fps screen
30fps screen
Flick to compare
The same flick on a 60fps and a 30fps screen.

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:

  1. 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, like x0 · e^(−kt).
  2. 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.
  3. Apply input at its timestamp

    Step up to the moment of the press, then apply it, so recorded input replays identically.
  4. Store what the person placed

    Keep the panel size they chose; compute the squeezed-to-fit version each frame without saving it.
  5. 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 loop

If 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.

Springs that feel right· for your agent
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?

What this lesson builds on and leads to, across every course.

  • Gestures, sheets and trays
  • Words matter
  • Briefs that work