# Spring Physics for Natural Motion

**SHOULD** · **ID:** `animations-spring-physics` · **Category:** animations
**Source:** Custom
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-spring-physics

> SHOULD: Prefer spring-based animations for interactive elements (buttons, modals, drag). Configure mass/tension/friction instead of duration/easing for natural feel and interruptible motion.

Prefer spring-based animations for interactive elements like buttons, modals, and drag

A duration-and-easing tween is a script: it always takes 300ms and always follows the same curve, whatever the element was doing a moment ago. Interrupt it — flick a drawer back while it is still closing — and it restarts from wherever it is with zero velocity, which reads as a stutter. A spring is a simulation: it carries the element's current velocity into the new target, so a reversal continues the gesture instead of contradicting it. That is the reason to reach for one, not "it feels bouncier". Configure it in Motion's modern terms — `{ type: "spring", duration: 0.5, bounce: 0.2 }` — rather than hand-tuning mass/stiffness/damping, and keep bounce low (see animations-emil-subtle-bounce): a spring with zero bounce is still a spring, and still interruptible.

## Bad — do not do this

`animations-spring-physics-bad`

```tsx
import { useState } from 'react';

export function SpringPhysicsBad() {
  const [on, setOn] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setOn((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Toggle (spam me)
      </button>
      <div className="h-20 flex items-center">
        <div
          className="size-14 rounded-xl bg-primary"
          style={{ transform: on ? 'translateX(160px)' : 'translateX(0)', transition: 'transform 300ms ease' }}
        />
      </div>
      <p className="text-xs text-destructive">
        CSS <code>ease</code> — no momentum or overshoot; spam-clicking restarts it abruptly from a standstill each time
      </p>
    </div>
  );
}
```

## Good — do this

`animations-spring-physics-good`

```tsx
import { useState } from 'react';
import { motion } from 'motion/react';

export function SpringPhysicsGood() {
  const [on, setOn] = useState(false);

  return (
    <div className="space-y-4">
      <button
        onClick={() => setOn((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground transition-colors duration-150 ease-out hover:bg-primary/90"
      >
        Toggle (spam me)
      </button>
      <div className="h-20 flex items-center">
        <motion.div
          animate={{ x: on ? 160 : 0 }}
          transition={{ type: 'spring', stiffness: 400, damping: 12 }}
          className="size-14 rounded-xl bg-primary"
        />
      </div>
      <p className="text-xs text-success">
        A spring with a little overshoot — spam-clicking redirects smoothly from the current velocity, never restarts
      </p>
    </div>
  );
}
```

## References

- [Motion — spring transitions](https://motion.dev/docs/react-transitions#spring)
- [MDN — linear() easing (approximating springs in CSS)](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/easing-function/linear)
