A decaying spring oscillation drawn in lime on a dark grid.
A decaying spring oscillation drawn in lime on a dark grid.

3 min read

Spring physics for UI motion, explained with Motion

Duration-based easing looks fine until something interrupts it. Springs don't have that problem. Here's the mental model, the two numbers that matter, and a live playground to build intuition.

Every animation you ship answers one question: how should this thing get from here to there? For years the default answer was a duration and an easing curve. 300ms ease-out. Ship it.

The trouble starts the moment something interrupts. A user taps a card mid-animation, or a menu closes before it finished opening. A duration-based animation has to either jump to the end or restart from scratch, and both look wrong. A spring just… keeps going. It has velocity, and the new target becomes the new resting point. That's the whole pitch.

The mental model#

Imagine the element is attached to its destination by a literal spring. Pull it away and let go. Two things determine what happens next:

  • Stiffness is how strong the spring is. Higher stiffness snaps back faster.
  • Damping is how much friction there is. Higher damping kills the bounce.

That's it. Mass exists too, but you'll rarely touch it — it's stiffness-in-disguise.

Three responses over time: underdamped, critically damped, overdamped.
Underdamped overshoots and settles. Critical settles fastest with no overshoot. Overdamped creeps in.

The ratio between the two is what your eye actually reads. Low damping relative to stiffness is underdamped — it overshoots and oscillates. Balanced is critically damped — fastest possible settle with no overshoot. Too much damping is overdamped — it eases in like it's tired.

Try it#

This is a live component rendered inside the post. Drag the sliders and hit replay. The code string underneath is exactly what you'd pass to Motion.

{ type: 'spring', stiffness: 320, damping: 18 }

A few things to notice while you play:

  1. Around stiffness 300 / damping 30 the box arrives quickly and stops dead. That's roughly critically damped, and it's the workhorse setting for layout changes.
  2. Drop damping to 12 and it bounces. Great for something playful — a like button, a toast — and exhausting for anything the user sees a hundred times a day.
  3. Push stiffness to 1000 and damping to 60. Notice it still feels alive even though it's very fast. Springs at high stiffness don't look mechanical the way a 100ms linear transition does.

The code#

In Motion, a spring is just a transition type:

components/card.tsx
'use client';
 
import { motion } from 'motion/react';
 
export function Card({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      whileHover={{ y: -4 }}
      transition={{
        type: 'spring',
        stiffness: 400,
        damping: 30,
      }}
      className="rounded-2xl border p-6">
      {children}
    </motion.div>
  );
}
tsx

The highlighted lines are the entire physics configuration. Everything above them is ordinary React. Note there's no duration — the spring decides when it's done.

Interruptions for free#

Here's the case that sells springs. Hover in, hover out, hover in again, quickly:

<motion.button
  whileHover={{ scale: 1.04 }}
  whileTap={{ scale: 0.96 }}
  transition={{ type: 'spring', stiffness: 500, damping: 32 }}>
  Save
</motion.button>
tsx

Each state change retargets the spring using its current velocity. There's no tween to cancel, so there's never a jump. With a duration-based transition you'd need to write that logic yourself, and you'd get it subtly wrong.

Presets worth memorising#

You don't need many. These four cover almost everything I ship:

Use forStiffnessDampingFeel
Layout shifts, sidebars, dialogs40030Fast, no overshoot
Hover / press feedback50032Snappy, barely-there bounce
Toasts, badges, playful confirmation30014Visible overshoot, one bounce
Large hero elements on page load12020Slow, weighty, settles softly

If you find yourself needing mass, try lowering stiffness first. Nine times out of ten it's the same effect with fewer knobs.

When not to use a spring#

  • Progress indicators. A spinner or progress bar should be linear. Physics on a loading state reads as a bug.
  • Anything synchronised to audio or video. You want deterministic timing there.
  • Colour changes. Springs on colour can overshoot into weird hues. Use a short tween.

Everything else — position, scale, opacity on things the user interacts with — defaults to a spring for me now. The difference is subtle in a screenshot and obvious in the hand.