A deep dive into spring physics in Motion (formerly Framer Motion) and how to create natural, buttery-smooth interactions.
When it comes to web animations, traditional CSS easing functions like ease-in or ease-out are often the default choice. However, they rely on fixed durations, which can make them feel rigid, mechanical, and disconnected from the real world.
Enter Spring Physics.
Springs are fundamentally different. Instead of animating over a set time, they simulate physical properties like mass, stiffness, and damping. This creates animations that feel organic, tactile, and buttery-smooth. With the library Motion (formerly Framer Motion), implementing spring physics is incredibly straightforward.
In the real world, objects don't move with perfect, linear precision. They carry momentum, they overshoot, and they bounce depending on their weight and the friction of their environment.
When you use a spring, you are describing the environment of the animation rather than the duration. This solves a few critical problems:
0.3s vs 0.4s or complex bezier curves. You just tweak physical properties until it feels right.Motion allows you to define a spring transition by simply setting type: "spring". To control the feel of the spring, you primarily adjust three properties:
Stiffness determines how "tight" or "strong" the spring is. A higher stiffness means the animation will move faster and snap to its destination more aggressively.
Damping acts as friction. It determines how quickly the spring stops bouncing.
Mass represents the "weight" of the object being animated. A heavier object takes longer to get moving and longer to stop.
To truly understand how these properties interact, the best way is to play with them. Use the visualizer below to adjust Stiffness, Damping, and Mass and see how it affects the curve and the resulting animation. Copy the config directly into your project when it feels right.
Here is how you can implement a basic spring animation on a button hover or tap state:
Notice how we don't define a duration. The animation takes exactly as long as the physics dictate.
bounce and duration ShorthandIf tweaking mass and stiffness feels too scientific, Motion provides an alternative, more intuitive API using bounce and duration.
bounce (0 to 1): Determines how bouncy the spring is. 0 means no bounce, 1 means highly bouncy.duration (in seconds): The approximate time it takes for the animation to settle.<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
type: "spring",
bounce: 0.4,
duration: 0.8,
}}
>
I'm a card dropping in!
</motion.div>Under the hood, Motion calculates the appropriate stiffness, damping, and mass to achieve this bounce and duration. This is often the best way to get started.
restDelta and restSpeedBy default, Motion considers a spring "at rest" when its position and velocity fall below a small built-in threshold. You can override both with restDelta and restSpeed.
restDelta — how close to the target (in px) the spring must be before it snaps.restSpeed — how slow (in px/s) the spring must be moving before it stops.transition={{
type: "spring",
stiffness: 200,
damping: 20,
restDelta: 0.01, // snap when within 0.01px of target
restSpeed: 0.5, // snap when moving slower than 0.5px/s
}}Lowering both makes the spring settle more precisely — useful for pixel-perfect layout animations. Raising them makes it snap earlier, which can feel snappier on cheap hardware at the cost of a tiny visual jump.
useSpring — Springs as Live ValuesEverything above applies to one-shot animate transitions. But Motion also exposes useSpring, which turns a MotionValue into a spring-smoothed version of it. This is how you build cursor followers, scroll-linked parallax, and drag-snap effects — things that need to react continuously to live input rather than animate between two fixed states.
Move cursor here
The key insight: mouseX and mouseY update instantly on every mousemove. smoothX and smoothY chase them with spring physics — no animate call, no keyframes, just continuous physical simulation driven by live values.
You can also initialize useSpring from a static number:
const scale = useSpring(1, { stiffness: 300, damping: 20 });
// Later, in an event handler:
scale.set(1.2); // springs to 1.2
scale.set(1); // springs backThis is where springs genuinely beat CSS transitions. When you change a spring's target mid-animation, it doesn't reset — it redirects from wherever it currently is, carrying its current velocity.
Rapid clicks cause abrupt direction changes because every animation restarts with a fresh easing curve.
Rapid clicks preserve momentum. The box smoothly redirects while carrying its existing velocity into the new direction.
Spam the buttons rapidly. The difference becomes obvious when animations are interrupted before they finish.
Spam the buttons rapidly and compare both rows. The tweened animation recalculates a brand-new easing curve on every interruption, which makes direction changes feel abrupt. The spring, however, preserves its current velocity and smoothly redirects momentum into the new direction — making the interaction feel continuous, responsive, and physical.
These are battle-tested starting points. Paste them in and tweak from there.
| Use case | Stiffness | Damping | Mass | Notes |
|---|---|---|---|---|
| Modal / sheet enter | 300 | 30 | 1 | No bounce, fast settle |
| Tooltip / popover | 500 | 25 | 0.5 | Light and snappy |
| Drag snap-back | 200 | 20 | 1.2 | Carries drag momentum |
| Cursor follower | 400 | 30 | 0.5 | Tight but smooth |
| Hero text drop-in | 120 | 14 | 1 | Gentle bounce |
| Bouncy button | 600 | 12 | 0.8 | Playful overshoot |
Springs are not universally better. A few cases where they hurt more than they help:
opacity and color — these properties don't have physical analogues. A bouncing fade-in looks broken, not natural. Use ease-out with a short duration instead.layout animations on text — spring-driven layout shifts on reflow-heavy text containers can cause jank on low-end devices. Prefer ease with layout prop.repeat: Infinity. Use a keyframe tween for spinners, pulses, and marquees.prefers-reduced-motion. A user who has enabled this setting finds bouncy animations actively uncomfortable. Fade to a simple opacity tween in that case.const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
const transition = prefersReduced
? { duration: 0.15, ease: "easeOut" }
: { type: "spring", stiffness: 400, damping: 25 };Springs aren't just for playful, bouncy UI. They are essential for creating professional, premium interfaces.
velocity into the spring, the card preserves the momentum of the user's swipe, creating a hyper-realistic physical interaction.Once you start using spring physics, it's hard to go back to standard CSS transitions. By shifting your mindset from "how long should this take" to "how heavy should this feel", you can elevate your web interfaces from feeling like static websites to feeling like native applications.
The three things worth internalizing: use type: "spring" with stiffness/damping/mass for one-shot animations, reach for useSpring whenever you need to smooth live input values, and always gate bouncy springs behind a prefers-reduced-motion check.
MotionValues