Motion Is Not Decoration, It Is Physics

Why springs feel real and timed animations feel fake

8 min read

Watch someone flick a card off the screen in a good app, then in a bad one. In both, the card leaves. In the good one, it leaves at the speed you threw it. In the bad one, it leaves in exactly 300 milliseconds, because someone typed 300 into a settings file. The interface just told you it was not really watching you.

A row of sequential 1878 photographs showing a horse and rider galloping, captured one frame at a time.
Muybridge's 1878 frames settled an argument nobody could win by eye: a galloping horse does lift all four hooves at once. Motion stopped being a matter of opinion and became something you could measure. That is the difference between a spring, which follows real physics, and a timed curve, which follows a number someone typed.Eadweard Muybridge, via Wikimedia CommonsPublic domain

The problem with fixed time

A timing curve maps how far along the animation is to how much time has passed. It is a neat idea with one fatal flaw: it has to know up front how long it will take. That is fine for something the app started on its own. It is wrong for anything your finger is touching. Your gesture has a speed, and a fixed-time animation has nowhere to put that speed.

This causes the interruption problem. You drag, you let go, and the animation jumps back to the start and runs a fixed curve. There is a visible jump where the object's speed suddenly changes. Think of a door on a spring versus a door on a timer. Push the spring door and let go, and it keeps going from where your hand left it. The timer door ignores your push and closes on its own schedule. Your eye catches that jump even when your mind does not. It reads as cheap.

Scripted motion: fixed time and easing
  • Knows its length before it starts
  • Ignores how fast your gesture moved
  • Jumps back to the start when interrupted
  • One curve for a toggle and a full screen
  • Time is a number you typed in
Physical motion: a spring
  • Its length just falls out of the maths
  • Keeps the speed you threw it with
  • Carries on from wherever it is now
  • Weight makes heavy things move heavily
  • Time comes out of stiffness, damping, weight
The two can look the same when still. They split apart the moment you grab one in mid-air.

Fixed time + easing

Spring

You let go
speed = 1200 px/s

Which model?

Back to speed 0
fixed 300 ms
visible jump

Carries on at 1200
time just happens
smooth

Feels fake

Feels real

New touch mid-air

Jumps / restarts

Bends smoothly

The difference is not about looks. A spring takes speed as an input, so you can grab it and change its target without any jump.

A spring does not take a time. It takes stiffness, damping, and weight, plus where the thing is right now and how fast it is moving. The time is a result: whatever comes out of the maths. Think of a drawer sliding shut. It speeds up, then eases into place on its own, and nobody set a timer for it. So you can throw new input at a spring in mid-air and it just changes course, because it was never following a clock.

typescript
const settle = {
  type: "spring",
  stiffness: 400,   // how hard it pulls toward the target
  damping: 34,      // how quickly oscillation dies (≈ critical here)
  mass: 1,          // perceived weight
}

onDragEnd={(_, info) => {
  animate(x, targetX, {
    ...settle,
    velocity: info.velocity.x,   // inherit the throw
  })
}}
The important line is the last one. Handing the gesture's speed to the spring is what makes the move smooth.

The maths, in plain words

A spring is three forces added together. Stiffness pulls the object toward its target. Damping slows it down. Mass makes it resist being moved at all. Nothing else is in the model. Once you can feel what each one does, tuning stops being guesswork.

Stiffness is the pull. It works like a stretched rubber band. The further the object sits from where it belongs, the harder it gets yanked back. Move it twice as far and it pulls twice as hard. High stiffness snaps home fast and eager. Low stiffness drifts in like a leaf settling.

Damping is the brake. It is the force that bleeds energy out of the motion, the way a shock absorber on a car soaks up a bump instead of letting it bounce. With no damping a spring would swing past its target and oscillate forever. With too much, the object crawls the last stretch and never seems to arrive.

Mass is weight. A heavy object is slow to start and slow to stop, because it carries momentum once it is moving. Give a card more mass and it leans into the motion like a loaded trolley, then takes a moment to give that speed back up.

The feel of the whole thing comes from the balance between damping and stiffness. Engineers call that ratio the damping ratio, and it splits every spring into one of three behaviours. Too little damping and it overshoots and wobbles. The right amount and it arrives as fast as possible with no overshoot at all. Too much and it is smooth but sluggish. This is the same maths that decides whether a car door thunks shut cleanly or bounces on its seal.

The damping ratio is damping measured against stiffness and mass. It is the one number that decides bounce versus composure. Most interface springs want to sit at or just below critical.
Damping ratioWhat happensWhen to use it
Under 1 (underdamped)Overshoots, then wobbles backA little life on a confirmation
Exactly 1 (critical)Arrives fastest with no overshootThe safe default for daily UI
Over 1 (overdamped)No overshoot, but slow and heavyLarge, calm, weighty surfaces
The damping ratio is damping measured against stiffness and mass. It is the one number that decides bounce versus composure. Most interface springs want to sit at or just below critical.

Notice what is missing from all of this: a duration. You never tell a spring how long to take. You set the three forces, hand it a starting position and a starting speed, and the time it takes is simply whatever falls out of the maths. Josh Comeau's walk through the physics is the clearest version of this I have read, and it is worth doing once with the sliders in front of you.

Why interruption is free for a spring

A spring survives your finger because it is defined by two live numbers: where the object is right now, and how fast it is moving right now. Add a target and that is the whole state. Change the target while the object is mid-flight and nothing resets. It keeps its position, keeps its speed, and simply starts heading somewhere new. The redirect is smooth because the speed was never thrown away.

A timed curve has no room for that. It is a function of one input: how long since it started. To interrupt it you either restart the clock, which snaps the speed back to zero and jumps, or you let it run to the old target and ignore the person entirely. There is no slot in the model for the speed the object already has. That single gap is the whole reason one feels alive under your finger and the other feels like a recording.

When a spring is the wrong tool

Springs are not always right, and pretending otherwise is how you end up fighting the tool. The honest rule is this: use a spring when a human is touching the thing, and use a timed curve when a clock is in charge. Any motion that must finish at a known moment belongs to the clock.

A progress bar that has to reach the end exactly when the upload completes cannot be a spring, because a spring's finish time drifts with its inputs. A three second onboarding where several elements must land together needs a shared timeline, not three springs that each arrive whenever their maths says so. A countdown, a timeout ring, anything with a promised duration: timed curve.

The sharpest case is motion tied to a timeline you do not own, which means audio and video. If a caption must appear on a beat, or a character's mouth must match a soundtrack, every frame maps to a fixed timestamp. A spring, whose timing is emergent, cannot be pinned to frame 240 of a clip. There you want keyframes on a timeline, the same tool animation has used for a century. Springs are for touch. Timelines are for time.

Tuning by feel, with a method

The same four questions, answered by each model. Only one column can take the speed of your gesture.
Fixed time + easingSpring
Can be interruptedJumps back to the startBends from where it is
FeelFake, all the sameReal, has weight
What you tuneA time and a curveStiffness, damping, weight
Keeps your speedNo, it is thrown awayYes, as a direct input
The same four questions, answered by each model. Only one column can take the speed of your gesture.
  • Stiffness sets urgency. Higher pulls harder toward the target. Below about 150 it feels sleepy. Above about 600 it feels snappy and a bit aggressive.
  • Damping sets composure. Too low and it wobbles like jelly, which feels like a toy. Damping that arrives fast with no wobble is the right default for anything a professional uses all day.
  • Weight sets scale. Heavy things should move heavy. Push a full shopping trolley and it rolls slowly; an empty one darts away. A full screen that moves as lightly as a toggle is the surest sign an interface was animated by a settings file, not a person.
  • A little bounce is a flavour, not a default. A tiny bounce on a confirmation feels nice. The same bounce on a table row feels broken.
typescript
// Physical: no duration, inherits velocity, interruptible
const spring = { type: "spring", stiffness: 400, damping: 34, mass: 1 }

// Scripted: the closest fixed-time approximation
const eased = {
  duration: 0.3,               // 300 ms, decided up front
  ease: [0.16, 1, 0.3, 1],     // ease-out: fast start, soft finish
}
The same settle, written both ways. Emil Kowalski's rule of thumb, ease out and usually under 300 ms, is what the curve below sets. The spring lands in the same place, but only it survives being interrupted.

Emil Kowalski makes the same case from the craft side. Great animations are fast, usually under 300 milliseconds, have a purpose, and above all can be interrupted. A user can reverse or redirect one in mid-air without ever seeing it jump. A timing curve can be made fast and pretty. It cannot be made truly interruptible, because interruption is a speed problem, and a curve has thrown the speed away.

What motion is actually for

Three jobs, and everything else is noise. It shows where something came from, so a new view feels like a continuation, not a replacement. It confirms your input registered, closing the loop inside the first tenth of a second. And it points your eye at the one thing that changed, which matters a lot in busy screens where a change can slip by.

Anything that does not do one of those three is decoration. And decoration in motion costs more than decoration in colour, because it eats time the user never agreed to spend.

Micro feedback (press, toggle)100-150 ms
Element transition (card, row)200-300 ms
View / route change350-500 ms
Anything over600 ms+ = friction
The time budgets I work to, by feel. Springs will not hit these exactly. The point is to tune stiffness and damping until the settle lands in the band.Kousik Dutta, working notes

Reduced motion is not an edge case

Somewhere between a third and a half of adults feel some motion sensitivity. For some people, a big sliding, layered transition is not just annoying. It brings on nausea that lasts hours. The prefers-reduced-motion setting, which lets people ask for less movement, is not a checkbox you add at the end. It is a second version of your motion, designed just as carefully.

Reduced motion does not mean no motion. It means no big movement across the screen, no layered scrolling, no growing from nothing. Fades and colour changes are usually fine and still carry the signal. Stripping out all motion leaves those users with an interface that never confirms anything, which is its own kind of failure.

css
@media (prefers-reduced-motion: reduce) {
  /* Keep the signal, remove the travel. */
  * {
    animation-duration: 1ms !important;
    transition-duration: 1ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .crossfade { transition: opacity 120ms linear !important; }
}

One more failure ships all the time: reveal-on-scroll animations that hide content behind a class toggle. Transitions do not fire on background tabs or in headless renderers. So the section shows up permanently invisible to a crawler, a screenshot tool, or anyone whose JavaScript failed. Start from a visible state and animate from there. Enhance, never gate.

Was this useful? Your choice stays private to this device.