Part II · Physics 05 / 25

05 Springs 05 Springs 05 Springs 05 Springs

A spring has no duration. It has physics, and time falls out of it.

In chapter 4 you drew a bounce by hand, choosing every height and every time. A spring lets physics choose them. To see why that matters, start with what a tween promises. A tween (from “in between”) is any animation with a fixed duration and an easing curve, such as a CSS transition or every curve so far in this course. Every tween makes a promise before it starts: I will be there in this long. Then the curve bends the path so the promise comes true. That works while nothing changes. But people change their minds. They tap again, grab the thing mid-flight, send it somewhere else. The promise is already made, and a tween has two bad choices: ignore the change, or start again from a standstill.

A spring makes no promise about time. It describes the thing instead: how hard it is pulled toward its place, how much it resists, how heavy it is. The motion follows from that, and the time follows the motion. A tween fixes the time and bends the path to fit. A spring fixes the physics and lets time follow.

Change its mind

Both dots chase the same target, the blue ring. Every so often the target jumps. Tap the figure to move it yourself. Under each row, a line traces the dot’s velocity. Watch what that line does at the moment of each jump.

000/360 fr
Open in Canvas sandbox
Fig. 5.1 — A tween and a spring, retargeted mid-flight

Watch each jump. The tween starts its new trip from a standstill: its velocity line drops straight to zero and climbs again, and the dot stalls before it turns. The spring never stalls. The pull changes direction, but the speed the dot already had is still there, so it curves into the new trip. The spring never needs to know how long the trip will take. It only needs to know where the target is now. So anything the user can interrupt (a drawer, a drag, a toggle) wants a spring, not a tween.

Which one lands in its place?

Both of these are springs, written as the spring(response, bounce) pair you will meet below. Response is how quick, and it is the same for both. Bounce is how much the thing overshoots, and it is the one thing that differs.

Feel

An item moves to the other list. Which one lands in its place?

Durations are a lie

A spring’s motion comes from one rule, applied again and again. The further the thing is from its target, the harder it is pulled back. The faster it moves, the more friction slows it. Nothing in that rule mentions time.

In the maths, a spring never quite stops. It only gets closer and closer. So its duration is a measurement, not a setting: the moment it is close enough and slow enough that nobody could see it move again. This site’s own springs, spring.snappy and spring.soft (its two spring tokens), settle in 474ms and 727ms. Nobody typed those numbers. They were computed from the physics.

Springs are a model

Picture a weight hanging from a coil spring, like a toy on a spring or a car on its suspension. Pull it down and let go: it bobs, and the bobbing dies away because of friction, like a shock absorber. Three numbers describe it:

  • Stiffness k: how hard the spring pulls. The pull grows with the distance from rest: twice as far, twice the pull. A stiffer spring is faster.
  • Damping c: friction that grows with speed, like moving through honey: the faster you go, the more it holds you back. It takes energy out of the motion. With too little, the weight swings past rest again and again.
  • Mass m: how heavy the weight is. A heavier weight is slower to get going and slower to stop.

Here x is the distance from the target and v is the velocity: how fast the weight is moving, and in which direction. At every instant the spring adds two forces, the pull −k · x and the friction −c · v, and divides by the mass to get the acceleration, the rate at which v changes: a = (−k·x − c·v) / m. Each frame, a program adds a · dt to v, then v · dt to x (dt is the seconds since the last frame). That is the whole model.

120
4.0
1.0
000/300 fr 0ms
Open in Canvas sandbox
Fig. 5.2 — A weight on a spring: stiffness, damping, mass

Every few seconds the figure pulls the weight down and lets go, and you can drag it yourself. With these settings it swings back and forth for a while, because there is almost no friction. Raise the damping to about 22 and it returns without passing the rest line: the friction now just beats the swing. Then double the mass: it gets slower, and it overshoots again, a little. Try stiffness too: higher is a faster, tighter swing.

That is the trouble with k, c and m as design tools: each one changes two things at once (speed and bounce). Stiffer means faster and bouncier, because the extra pull arrives while friction stays the same. Heavier means slower and bouncier. More damping means less bounce, but past a point it also means slower. You cannot say “keep the tempo, remove the bounce” by touching one of them.

The designer’s pair

A designer wants two separate dials: how quick, and how bouncy. So the same spring is often described with two other numbers.

  • Response: the spring’s tempo, in seconds. It is the time one full swing would take with no friction. Lower is snappier.
  • Bounce: how much it overshoots. 0 is none, and values toward 1 swing more and more. Below 0, it creeps into place without overshooting.

They convert exactly to the physics (Apple’s model, the one this site uses). ζ (zeta) is the damping ratio, explained in the next section: 1 means no overshoot, less than 1 means overshoot.

  • k = (2π / response)² · m
  • c = 4π · ζ · m / response
  • ζ = 1 − bounce, or ζ = 1 / (1 + bounce) when bounce is below zero.

Where they come from: a weight on a spring swings once every 2π · √(m / k) seconds with no friction. Set that equal to the response and solve for k, and you get the first line. The third line just turns the bounce dial into damping ratio, and the second line turns that ratio into friction.

Three springs, both ways, with m = 1:

Springresponse · bouncek · c
spring.snappy0.3 · 0438.7 · 41.89
spring.soft0.5 · 0.15157.9 · 21.36
A playful pop0.4 · 0.3246.7 · 21.99

Mass drops out here. In the designer’s pair, k and c both grow with m, so every mass gives the same motion and there is no mass dial. (In the k · c · m picture above, you held k and c fixed, so a heavier weight really did change the motion.) And each dial does one job. Change the response and the overshoot stays exactly the same; the whole motion just plays faster or slower. Change the bounce and the stiffness stays put; only the friction changes. A rule of thumb: with no bounce, a spring settles in about 1.5 times its response, so response 0.3 settles in roughly 450ms. Start there when you need a duration to budget for.

When a bounce is honest

Behind the bounce is one number, the damping ratio ζ. In plain words:

  • ζ below 1: underdamped. It overshoots, swings back, and settles inside a shrinking envelope. Bounce above 0.
  • ζ equal to 1: critically damped. The fastest return that never overshoots. Bounce 0.
  • ζ above 1: overdamped. No overshoot, but it creeps into place. Bounce below 0.
0ms250ms500ms750ms1s1.25s1.5s1.75s01targetsettles · 699msovershoot 5%
bounce 0.3 · underdampedbounce 0 · criticalbounce −0.5 · overdamped
00/60 fr 0ms
Fig. 5.3 — Same response, three damping ratios

With ζ at 0.7, the underdamped spring overshoots by about 5% and swings back inside its dashed envelope. It settles in 699ms. The critically damped one, ζ 1, never crosses the target line and comes to rest first, in 612ms. The overdamped one, ζ 2, takes 1658ms.

An overshoot is a claim: this thing was moving too fast to stop at its target. It says the thing has momentum. When that is true, the bounce is honest:

  • Something the user threw. A card flicked away, a sheet let go with speed. The momentum came from the finger.
  • A playful confirmation. A like, a badge landing, a check mark. The energy is the message.

When it is false, the bounce is a lie:

  • Things with no reason to move fast. A tooltip, a modal backdrop, a fade. Nothing pushed them.
  • Values. A bar in a chart that overshoots says, for a moment, that the number was higher than it is. A progress bar that bounces past the end claims more than done.

This site follows the rule. spring.snappy has no bounce and is used for direct manipulation. spring.soft has a little, 0.15, and is kept for playful confirmations.

A spring remembers its speed

A spring’s whole state is two numbers: where it is, and how fast it is going. Give it a new target and it carries on from that state. There is nothing to restart, because there was never a clock. That is what you saw in Figure 5.1.

The same fact means a spring can start already moving. Hand it the velocity of a finger at release, and it carries that motion into its settle.

0ms100ms200ms300ms400ms500ms600ms700ms01targetsettles · 587msovershoot 11%
00/60 fr 0ms
Fig. 5.4 — Bounce 0, from rest and already moving

Both springs are critically damped. Look at the ends of the two curves. The blue one starts at rest and never passes its target. The one with the red dot starts already moving toward the target, as if flicked (30 trip-lengths per second, about a thirtieth of a second to cross the whole trip if nothing slowed it). It arrives with so much speed that it overshoots by about 11%, with no bounce at all. That overshoot is honest: it is the finger’s momentum, not the spring’s. It’s why a spring for direct manipulation can have bounce 0 and still feel physical.

A tween has none of this. Its state is a clock. Interrupt it and it starts a new clock, and its speed jumps to whatever its curve starts with, not the speed it had. The kink in Figure 5.1 is that jump.

Two dials

The bench starts at response 0.4, bounce 0.3. Drag the bounce down to 0 and watch settles in drop from 699ms to 612ms: less wobble, so it comes to rest sooner. Take it below 0 and watch the curve creep, never crossing the target. Then change the response: the shape holds, only the tempo moves. Last, switch Describe it as to k · c · m, where ζ in the readout tells you how bouncy it is (under 1 overshoots), and double the mass. It gets slower and bouncier at once.

L3 Spring bench
Open in Lab
Presets
Describe it as
0.40s
time of one swing; lower is snappier
0.30
0 = no overshoot · above bounces · below 0 creeps in (overdamped)
ζ damping 0.70 · underdamped · overshoots k c m 247 · 22.0 · 1.0 response 0.40s · bounce 0.30 settles in 699ms
Compare with B
0ms100ms200ms300ms400ms500ms600ms700ms800ms01targetsettles · 699msovershoot 5%
spring
CSS · linear() · 26 stops · 699ms linear(0, 0.0057 1%, 0.0154 1.67%, 0.0291 2.33%, 0.0669 3.67%, 0.1157 5%, 0.1578 6%, 0.219 7.33%, 0.4836 12.67%, 0.592…
00/42 fr 0ms

The notation

CSS has no spring, but linear() can draw one. Sample the spring into stops, then run it for exactly its settle time. A different duration would stretch or squeeze the spring into something else. The Spring bench exports this pair, and this site’s own tokens are made the same way: --spring-snappy is the curve and --spring-snappy-dur is its 474ms.

A linear() spring is still a tween, though. It has the spring’s shape but not its memory: interrupt it and it restarts. When a motion can be interrupted, run the spring in JavaScript.

05 · Springs: a spring as CSS linear() Open in Lab
/* response 0.4, bounce 0.3 → k 246.7, c 21.99, m 1.
   Sampled into linear() over its settle time, 699ms.
   Each stop is the spring's position (0 = start, 1 = target) at a
   point in time. Values above 1 are the overshoot: up to 1.046,
   4.6% past the target, before it settles back to 1. */
:root {
  --spring-pop: linear(0, 0.0057 1%, 0.0154 1.67%, 0.0291 2.33%,
    0.0669 3.67%, 0.1157 5%, 0.1578 6%, 0.219 7.33%, 0.4836 12.67%,
    0.5929 15%, 0.7048 17.67%, 0.7887 20%, 0.859 22.33%, 0.893 23.67%,
    0.9228 25%, 0.9706 27.67%, 1.0077 30.67%, 1.0243 32.67%, 1.0369 35%,
    1.0442 37.67%, 1.046 40.33%, 1.0408 45%, 1.0114 59%, 1.0019 67%,
    0.9979 78.33%, 1);
  --spring-pop-dur: 699ms;
}

.badge {
  transform: scale(0);
  transition: transform var(--spring-pop-dur) var(--spring-pop);
}

.badge.is-shown {
  transform: scale(1);
}
05 · Springs: the same spring in WAAPI Open in Lab
// linear() as the easing, the settle time as the duration:
// change the duration and the spring plays faster or slower, but no longer
// matches its physics.
const pop = {
  duration: 699,
  easing: `linear(0, 0.0057 1%, 0.0154 1.67%, 0.0291 2.33%, 0.0669 3.67%,
    0.1157 5%, 0.1578 6%, 0.219 7.33%, 0.4836 12.67%, 0.5929 15%,
    0.7048 17.67%, 0.7887 20%, 0.859 22.33%, 0.893 23.67%, 0.9228 25%,
    0.9706 27.67%, 1.0077 30.67%, 1.0243 32.67%, 1.0369 35%, 1.0442 37.67%,
    1.046 40.33%, 1.0408 45%, 1.0114 59%, 1.0019 67%, 0.9979 78.33%, 1)`,
};

badge.animate(
  [{ transform: 'scale(0)' }, { transform: 'scale(1)' }],
  { ...pop, fill: 'both' },
);
05 · Springs: an interruptible spring in Motion Open in Lab
import { animate } from 'motion';

// spring.snappy (response 0.3, bounce 0) as physics: the k, c and m
// from the table above, so no duration is given. Motion works it out.
const snappy = { type: 'spring', stiffness: 438.7, damping: 41.89, mass: 1 };
let open = false;

// Tap again mid-flight: the new spring starts where the panel is,
// at the speed it already has. Nothing restarts.
// x is in px: 240 is open, 0 is closed.
toggle.addEventListener('click', () => {
  open = !open;
  animate(panel, { x: open ? 240 : 0 }, snappy);
});

A spring can take the speed of a finger, but it cannot say where a fast flick should end up. Chapter 6 asks that: given a release speed and some friction, where does the thing stop?

Tune the spring to match

A spring plays in blue. Tune the response and the bounce until yours moves the same way. Read the tempo first, then the overshoot: they are separate dials.

Eye trainer · estimate Tune to match

Match the blue spring with your red one. Response is how quickly it arrives, bounce how far it overshoots. Use your eyes, then submit.Springs: response and bounce as feel.

5 trials. Judge with your eyes first; the numbers come after. Three right in a row makes trials harder, a miss eases them.

Full rounds and your calibration in the Eye trainer