| name | motion-physics |
| description | Physics-based UI animation system. Spring physics, easing curves, micro-interaction patterns, and timeline staggering for native-quality web animations. Sources: popmotion, framer/motion, anime.js, greensock, argyleink/transition.css, and 5 others. |
/motion-physics
When to Use
- UI transitions that feel mechanical or abrupt
- Micro-interactions (button press, toggle, checkbox)
- Page transitions and route animations
- Loading states that need to feel alive
Do NOT use for
- Animations that run for > 5s (use video/Lottie)
- Purely decorative background animations with no functional meaning
- Reduced-motion users (always check
prefers-reduced-motion)
The Physics Model (popmotion + react-spring)
Real-world objects have mass, tension, and friction.
Spring physics produces motion that feels natural because it mirrors reality.
const springs = {
snappy: { mass: 1, tension: 300, friction: 20 },
gentle: { mass: 1, tension: 120, friction: 14 },
wobbly: { mass: 1, tension: 180, friction: 12 },
stiff: { mass: 1, tension: 400, friction: 26 },
}
import { useSpring, animated } from '@react-spring/web'
const props = useSpring({ opacity: visible ? 1 : 0, config: springs.gentle })
Duration Rules (greenbadge/motion-spec)
Duration MUST scale with the physical distance traveled:
Micro interactions (< 4px move): 100–150ms
Small components (4–100px): 150–250ms
Large panels / modals (> 100px): 250–400ms
Page-level transitions: 300–500ms
Hard limits:
- Nothing interactive > 500ms (users perceive as slow)
- Nothing decorative that blocks interaction
- Entrance: faster than exit (snappy in, graceful out)
Easing Curves (argyleink/transition.css)
:root {
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.6, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.enter { animation: fade-in 200ms var(--ease-out) forwards; }
.exit { animation: fade-out 150ms var(--ease-in) forwards; }
Micro-Interaction Patterns (delightful-ui)
.button {
transition: transform 100ms var(--ease-spring),
box-shadow 100ms var(--ease-out);
}
.button:active {
transform: scale(0.97);
box-shadow: var(--elevation-1);
}
.toggle-thumb {
transition: transform 200ms var(--ease-spring);
}
.toggle:checked .toggle-thumb { transform: translateX(20px); }
@keyframes ripple {
from { transform: scale(0); opacity: 0.4; }
to { transform: scale(4); opacity: 0; }
}
Staggered List Animations (anime.js pattern)
import anime from 'animejs'
anime({
targets: '.list-item',
opacity: [0, 1],
translateY: [20, 0],
easing: 'easeOutExpo',
duration: 300,
delay: anime.stagger(50, { start: 100 })
})
Rule: Stagger delay must be proportional — longer lists = smaller delay per item.
Max total stagger time = 400ms regardless of list length.
Lottie for Complex Animations
import lottie from 'lottie-web'
const anim = lottie.loadAnimation({
container: document.getElementById('lottie'),
renderer: 'svg',
loop: false,
autoplay: true,
path: '/animations/success.json'
})
anim.setSpeed(1.5)
Use Lottie for: empty states, success/error illustrations, loading indicators.
Reduced Motion (mandatory)
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This override MUST be present in every project using animations.
Anti-Pattern Checklist
❌ Linear easing on any UI animation (always use curve)
❌ Duration > 500ms on interactive elements
❌ Entrance and exit using same easing (should be mirrored)
❌ Animating width/height directly (use transform: scaleX/scaleY)
❌ Missing prefers-reduced-motion media query
❌ Stagger total duration > 400ms
❌ Animating box-shadow directly (use opacity on pseudo-element)