Motion Is Not Decoration, It Is Physics
Why springs feel real and timed animations feel fake
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.

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.
- 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
- 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
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.
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 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.
| Damping ratio | What happens | When to use it |
|---|---|---|
| Under 1 (underdamped) | Overshoots, then wobbles back | A little life on a confirmation |
| Exactly 1 (critical) | Arrives fastest with no overshoot | The safe default for daily UI |
| Over 1 (overdamped) | No overshoot, but slow and heavy | Large, calm, weighty surfaces |
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
| Fixed time + easing | Spring | |
|---|---|---|
| Can be interrupted | Jumps back to the start | Bends from where it is |
| Feel | Fake, all the same | Real, has weight |
| What you tune | A time and a curve | Stiffness, damping, weight |
| Keeps your speed | No, it is thrown away | Yes, as a direct input |
- 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.
// 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
}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.
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.
@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.