# Springs

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

Module: Motion · Level: intermediate · 8 min · https://school.agents.org.in/design/motion/springs

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.

*[Interactive demo: Tune the spring, then interrupt it mid-flight.]*

A curve says "get there in 200ms, with this shape". A spring (Motion driven by physics instead of a fixed duration: a pull toward the target plus friction. It keeps its speed when interrupted, which makes drags and flicks feel natural) 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.

> **In plain words.** **Stiffness** is how hard the spring pulls toward its target: higher is snappier. **Damping** is friction: lower lets it overshoot and wobble, higher settles it calmly. **Mass** is how heavy the thing feels; leave it at 1.

## 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:

```ts
const settle = { type: "spring", duration: 0.35, bounce: 0 };
```

**The drawer spring** settles a sheet or drawer after you let go of it:

```ts
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.

*[Interactive demo: 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:

  **Step: 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)`.

  **Step: 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.

  **Step: Apply input at its timestamp.** Step up to the moment of the press, then apply it, so recorded input replays identically.

  **Step: Store what the person placed.** Keep the panel size they chose; compute the squeezed-to-fit version each frame without saving it.

  **Step: 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 lerp (Short for linear interpolation: moving a value a fixed fraction of the remaining way toward its target each frame, like 8%. Used to make live data glide instead of jump) of 8% per frame:

```ts
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 (paste into your agent):**

```text
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.
```

**Check yourself:** Which of these should use a spring rather than a cubic-bezier curve?

- [ ] A modal that opens when a button is clicked
- [x] A bottom sheet the user drags and releases
- [ ] A tooltip fading in
- [ ] A page transition after a link click

*Why:* The sheet is driven by the user's finger, can be grabbed again mid-flight and starts with real velocity. That's exactly where springs beat fixed-duration curves.

## In the skill

- https://school.agents.org.in/skill/motion/spring-animations.md
- https://school.agents.org.in/skill/motion/frame-rate-independence.md
- https://school.agents.org.in/skill/motion/lerp-breathing.md
