Spring Physics Deep Dive
Understanding and tuning spring animations for natural, physically accurate motion.
Table of Contents
- Physics Fundamentals
- Spring Parameters
- Tuning Guide
- Common Configurations
- Velocity and Momentum
- Advanced Techniques
Physics Fundamentals
What is a Spring Animation?
Unlike duration-based animations (e.g., "move from A to B in 300ms"), spring animations simulate physical motion based on forces:
Hooke's Law:
F = -k * x
Where:
F = Force applied by spring
k = Spring constant (stiffness)
x = Displacement from rest positionDamping Force:
F_damping = -c * v
Where:
c = Damping coefficient (friction)
v = VelocityResult: Natural, bouncy motion that automatically calculates duration based on physics.
Spring Parameters
Mass
What it does: Determines object's weight/inertia.
Effect:
- Higher mass (e.g., 5): Heavier, slower to accelerate/decelerate
- Lower mass (e.g., 0.5): Lighter, more responsive
When to increase:
- Large UI elements (modals, panels)
- Dramatic, weighty animations
- Slow, deliberate movements
When to decrease:
- Small UI elements (tooltips, badges)
- Quick, snappy interactions
- Lightweight, responsive feel
Example:
// Heavy modal
{ mass: 5, tension: 170, friction: 26 }
// Lightweight tooltip
{ mass: 0.5, tension: 170, friction: 26 }Tension (Stiffness)
What it does: Spring's resistance to stretching (spring strength).
Effect:
- Higher tension (e.g., 300): Faster, stiffer spring
- Lower tension (e.g., 100): Slower, softer spring
When to increase:
- Quick, responsive interactions
- Snappy button feedback
- Alert animations
When to decrease:
- Gentle, smooth transitions
- Organic, flowing motion
- Relaxed page transitions
Example:
// Snappy button press
{ tension: 300, friction: 20 }
// Gentle fade-in
{ tension: 120, friction: 14 }Friction (Damping)
What it does: Opposing force that slows motion.
Effect:
- Higher friction (e.g., 40): Less overshoot, faster settling
- Lower friction (e.g**, 10): More bounce, oscillation
When to increase:
- Minimal overshoot desired
- Professional, controlled feel
- Quick settle time
When to decrease:
- Playful, bouncy animations
- Noticeable spring effect
- Dramatic overshoot
Example:
// Minimal bounce (controlled)
{ tension: 170, friction: 40 }
// Bouncy, playful
{ tension: 180, friction: 12 }Tuning Guide
Step-by-Step Tuning Process
1. Start with a preset:
import { config } from '@react-spring/web'
// Try these first:
config.default // Balanced
config.gentle // Smooth, slow
config.wobbly // Bouncy
config.stiff // Fast, minimal bounce2. Adjust tension for speed:
// Too slow? Increase tension
{ tension: 250, friction: 26 }
// Too fast? Decrease tension
{ tension: 120, friction: 26 }3. Adjust friction for bounce:
// Too bouncy? Increase friction
{ tension: 170, friction: 35 }
// Not bouncy enough? Decrease friction
{ tension: 170, friction: 15 }4. Adjust mass for weight:
// Feels too light? Increase mass
{ mass: 3, tension: 170, friction: 26 }
// Too heavy/sluggish? Decrease mass
{ mass: 0.7, tension: 170, friction: 26 }Visual Tuning Tool
React Spring provides an interactive tuning tool:
import { useSpring, animated } from '@react-spring/web'
import { useControls } from 'leva'
function Tuner() {
const config = useControls({
mass: { value: 1, min: 0.1, max: 10 },
tension: { value: 170, min: 1, max: 500 },
friction: { value: 26, min: 1, max: 100 }
})
const springs = useSpring({
from: { x: 0 },
to: { x: 100 },
config
})
return <animated.div style={springs} />
}Install leva: npm install leva
Common Configurations
By Use Case
Button Interactions (Quick, Responsive):
{ mass: 1, tension: 300, friction: 20 }
// Fast response, minimal bounceModal Animations (Dramatic, Smooth):
{ mass: 2, tension: 150, friction: 30 }
// Weighty, controlled entrancePage Transitions (Smooth, Professional):
{ mass: 1, tension: 120, friction: 14 }
// Gentle, flowing motionNotifications/Toasts (Bouncy, Attention-Grabbing):
{ mass: 1, tension: 180, friction: 12 }
// Noticeable bounce, playfulDrag-and-Drop (Natural Physics):
{ mass: 1, tension: 200, friction: 25 }
// Balanced, realistic feelLoading Indicators (Continuous, Smooth):
{ mass: 1, tension: 100, friction: 30 }
// Slow, controlled oscillationBy Animation Feel
Snappy:
{ mass: 0.8, tension: 300, friction: 20 }Bouncy:
{ mass: 1, tension: 180, friction: 12 }Smooth/Gentle:
{ mass: 1, tension: 120, friction: 14 }Stiff/Controlled:
{ mass: 1, tension: 210, friction: 20 }Slow/Lazy:
{ mass: 1, tension: 280, friction: 60 }Heavy/Dramatic:
{ mass: 5, tension: 170, friction: 26 }Velocity and Momentum
Initial Velocity
Set starting velocity for momentum-based animations:
const [springs, api] = useSpring(() => ({
x: 0,
config: { tension: 200, friction: 20 }
}), [])
api.start({
x: 100,
velocity: 500 // px/second
})Preserving Velocity (Interruptions)
Maintain momentum when interrupting an animation:
const handleInterrupt = () => {
api.start({
x: newTarget,
velocity: springs.x.getVelocity(), // Current velocity
config: { tension: 300, friction: 25 }
})
}Why this matters:
- Natural feel when user changes direction mid-animation
- Smooth transitions between gesture states
- Realistic physics simulation
Calculating Velocity from Gestures
import { useDrag } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/web'
function DraggableElement() {
const [{ x }, api] = useSpring(() => ({ x: 0 }))
const bind = useDrag(({ down, movement: [mx], velocity: [vx] }) => {
api.start({
x: down ? mx : 0,
velocity: vx * 1000, // Convert to px/second
immediate: down,
config: { tension: 200, friction: 30 }
})
})
return <animated.div {...bind()} style={{ x }} />
}Advanced Techniques
Conditional Spring Config
Different physics for different properties:
const springs = useSpring({
x: 100,
scale: 1.5,
opacity: 1,
config: {
x: { tension: 300, friction: 20 }, // Fast horizontal
scale: { mass: 4, friction: 10 }, // Heavy scaling
opacity: { tension: 120, friction: 14 } // Gentle fade
}
})Config as Function
Dynamic configuration based on property:
const springs = useSpring({
x: 100,
y: 200,
scale: 1.5,
config: (key) => {
if (key === 'scale') {
return { mass: 4, friction: 10 }
}
if (key === 'x') {
return { tension: 300, friction: 20 }
}
return config.default
}
})Spring with Clamp
Prevent overshoot entirely:
const springs = useSpring({
x: 100,
config: {
tension: 300,
friction: 20,
clamp: true // No overshoot
}
})Duration Override
Force a specific duration (not physically accurate):
const springs = useSpring({
x: 100,
config: {
duration: 500, // Override physics
easing: t => t // Linear easing
}
})Note: When using duration, the animation is no longer physics-based. It becomes a traditional tween.
Precision Tuning
Control when animation "completes":
const springs = useSpring({
x: 100,
config: {
tension: 170,
friction: 26,
precision: 0.01 // Stop when within 0.01 of target (default: 0.0001)
}
})Higher precision (0.1):
- Fewer updates
- Earlier completion
- Better performance
- Slight imprecision
Lower precision (0.0001):
- More updates
- Exact final value
- Slower performance
- Pixel-perfect
Troubleshooting
Animation Too Fast
Solutions:
- Decrease
tension(e.g., 170 → 120) - Increase
mass(e.g., 1 → 2) - Increase
friction(e.g., 26 → 40)
Animation Too Slow
Solutions:
- Increase
tension(e.g., 170 → 250) - Decrease
mass(e.g., 1 → 0.7) - Decrease
friction(e.g., 26 → 15)
Too Much Bounce/Overshoot
Solutions:
- Increase
friction(e.g., 26 → 40) - Enable
clamp: true - Use
config.stiffpreset
Not Enough Bounce
Solutions:
- Decrease
friction(e.g., 26 → 12) - Use
config.wobblypreset - Increase
tensionslightly (e.g., 170 → 180)
Animation Never Completes
Solutions:
- Increase
precision(e.g., 0.0001 → 0.01) - Check for conflicting animations
- Ensure
restSpeedis not set too low
Mathematical Relationships
Critical Damping
The point where spring returns to rest without oscillating:
c_critical = 2 * sqrt(k * m)
Where:
c = friction
k = tension
m = massDamping Ratio:
ζ = c / (2 * sqrt(k * m))
ζ > 1: Over-damped (no overshoot, slow)
ζ = 1: Critically damped (no overshoot, fast)
ζ < 1: Under-damped (overshoot, oscillation)Example:
// Calculate critically damped friction
const mass = 1
const tension = 170
const criticalFriction = 2 * Math.sqrt(tension * mass)
// criticalFriction ≈ 26 (default React Spring config!)
// Under-damped (bouncy)
{ mass: 1, tension: 170, friction: 12 } // ζ ≈ 0.46
// Critically damped
{ mass: 1, tension: 170, friction: 26 } // ζ = 1
// Over-damped (slow)
{ mass: 1, tension: 170, friction: 40 } // ζ ≈ 1.53Comparison: Duration vs Physics
Duration-Based (Traditional)
// CSS transition
transition: all 300ms ease-in-out;
// React Spring duration override
{ duration: 300, easing: easeInOut }Pros:
- Predictable timing
- Easier to sync with other events
- Consistent across devices
Cons:
- Feels artificial when interrupted
- Loses momentum on direction change
- Not physically accurate
Physics-Based (Spring)
{ mass: 1, tension: 170, friction: 26 }Pros:
- Natural, organic feel
- Interruptible without jarring
- Preserves momentum
- Realistic physics
Cons:
- Variable duration (depends on distance, velocity)
- Harder to sync precisely
- Requires tuning for desired feel
Best Practices
- Start with presets - Use
config.default,config.gentle, etc. - Tune visually - Use Leva or similar tool to adjust in real-time
- Preserve velocity - Use
getVelocity()for interruptions - Set appropriate precision - Higher for better performance
- Test on target devices - Physics may feel different on slow devices
- Use duration sparingly - Only when exact timing is critical
- Document custom configs - Add comments explaining physics choices
- Consistency matters - Use similar configs for similar interactions
Resources
- Spring Physics Calculator - Interactive tuning
- Leva - Real-time config editor
- Hooke's Law - Physics fundamentals
- Damped Harmonic Oscillator - Spring theory