Skip to main content

React Motion Values Animation

Motion values are numeric properties that drive animations, separate from React state. They can be updated without triggering re-renders, making them ideal for animations that fire 60 times per second. Physics-based motion values add velocity and inertia—when a user drags a slider or flicks a card, the animation continues naturally beyond the user's input, decelerating smoothly. This creates the "weighty" feeling of modern UI.

Understanding Motion Values and Velocity

A motion value is a number that changes over time. You update it imperatively (not through state), and animations depend on it. Velocity is the rate of change of a position—the higher the velocity, the faster the animation. Here's a custom hook that demonstrates the concept:

import { useRef, useEffect, useState } from 'react';

export function useMotionValue(initialValue = 0) {
const valueRef = useRef(initialValue);
const [, setDummy] = useState(0);
const updateListenersRef = useRef(new Set());

const get = () => valueRef.current;
const set = (newValue) => {
valueRef.current = newValue;
updateListenersRef.current.forEach((listener) => listener());
};

const subscribe = (listener) => {
updateListenersRef.current.add(listener);
return () => updateListenersRef.current.delete(listener);
};

return { get, set, subscribe };
}

// Example: Simple counter that updates a motion value
export default function MotionValueCounter() {
const motionValue = useMotionValue(0);
const [display, setDisplay] = useState(0);

useEffect(() => {
const unsubscribe = motionValue.subscribe(() => {
setDisplay(motionValue.get());
});
return unsubscribe;
}, [motionValue]);

const increment = () => motionValue.set(motionValue.get() + 1);

return (
<div style={{ padding: '20px' }}>
<p>Motion Value: {display}</p>
<button onClick={increment} style={{ padding: '8px 16px', cursor: 'pointer' }}>
Increment
</button>
</div>
);
}

This basic hook shows how motion values decouple animation updates from React's render cycle. In real applications, you'd use a library like Framer Motion that provides optimized motion values.

Inertial Scrolling with Velocity Decay

Inertial scrolling mimics the feel of native mobile scrolling: the element continues moving after the user releases, gradually slowing down. Calculate velocity from the last two positions, then apply a decay factor:

import { useRef, useEffect, useState } from 'react';

export default function InertialScroll() {
const [position, setPosition] = useState(0);
const posRef = useRef(0);
const velocityRef = useRef(0);
const lastPosRef = useRef(0);
const lastTimeRef = useRef(Date.now());
const animationRef = useRef(null);

const handlePointerDown = (e) => {
lastPosRef.current = e.clientY;
lastTimeRef.current = Date.now();
velocityRef.current = 0;

if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};

const handlePointerMove = (e) => {
const currentTime = Date.now();
const deltaY = e.clientY - lastPosRef.current;
const deltaTime = Math.max(currentTime - lastTimeRef.current, 1);

velocityRef.current = deltaY / deltaTime;
posRef.current += deltaY;
setPosition(posRef.current);

lastPosRef.current = e.clientY;
lastTimeRef.current = currentTime;
};

const handlePointerUp = () => {
let currentVelocity = velocityRef.current;
const decay = 0.95; // Decay factor per frame

const animateInertia = () => {
currentVelocity *= decay;

if (Math.abs(currentVelocity) > 0.1) {
posRef.current += currentVelocity;
setPosition(posRef.current);
animationRef.current = requestAnimationFrame(animateInertia);
} else {
velocityRef.current = 0;
}
};

if (Math.abs(currentVelocity) > 0.5) {
animationRef.current = requestAnimationFrame(animateInertia);
}
};

return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
width: '100%',
height: '300px',
border: '2px solid #ccc',
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: '#f9f9f9',
cursor: 'grab',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
top: `${position}px`,
left: '50%',
transform: 'translateX(-50%)',
width: '80px',
height: '80px',
backgroundColor: '#3b82f6',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontWeight: 'bold',
userSelect: 'none',
}}
>
Drag me
</div>
</div>
);
}

The decay factor (0.95) multiplies the velocity each frame, so the movement gradually slows. Adjust the decay to make it feel more or less "bouncy."

Elastic Animation with Spring Physics

Spring physics creates natural, bouncy animations. A spring has stiffness (how fast it pulls back), damping (how quickly it stops bouncing), and mass. Here's a simplified spring animation:

import { useRef, useEffect, useState } from 'react';

export default function SpringAnimation() {
const [position, setPosition] = useState(0);
const [target, setTarget] = useState(0);
const posRef = useRef(0);
const velocityRef = useRef(0);
const animationRef = useRef(null);

const animateSpring = (stiffness = 0.1, damping = 0.2, mass = 1) => {
const animate = () => {
const delta = target - posRef.current;
const springForce = delta * stiffness;
const dampingForce = -velocityRef.current * damping;
const acceleration = (springForce + dampingForce) / mass;

velocityRef.current += acceleration;
posRef.current += velocityRef.current;

setPosition(posRef.current);

if (Math.abs(delta) > 0.01 || Math.abs(velocityRef.current) > 0.01) {
animationRef.current = requestAnimationFrame(animate);
}
};

if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
animationRef.current = requestAnimationFrame(animate);
};

const handleClick = (newTarget) => {
setTarget(newTarget);
animateSpring(0.1, 0.2, 1);
};

return (
<div style={{ padding: '40px' }}>
<div
style={{
width: '100%',
height: '200px',
border: '2px solid #ccc',
borderRadius: '8px',
position: 'relative',
backgroundColor: '#f9f9f9',
}}
>
<div
style={{
position: 'absolute',
left: `${position}px`,
top: '50%',
transform: 'translateY(-50%)',
width: '40px',
height: '40px',
backgroundColor: '#10b981',
borderRadius: '50%',
}}
/>
</div>

<div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
<button
onClick={() => handleClick(50)}
style={{ padding: '8px 16px', cursor: 'pointer' }}
>
Move to 50px
</button>
<button
onClick={() => handleClick(150)}
style={{ padding: '8px 16px', cursor: 'pointer' }}
>
Move to 150px
</button>
<button
onClick={() => handleClick(0)}
style={{ padding: '8px 16px', cursor: 'pointer' }}
>
Reset
</button>
</div>
</div>
);
}

Increase stiffness for snappier springs, increase damping to reduce bounce, and adjust mass to change how "heavy" the animation feels.

Key Takeaways

  • Motion values update imperatively without triggering React re-renders, ideal for 60 FPS animations driven by gestures or physics.
  • Velocity is calculated from position deltas and time deltas: velocity = (currentPos - lastPos) / (currentTime - lastTime).
  • Inertial decay (velocity multiplied by a decay factor each frame) creates natural, slowing motion after a user drag or swipe.
  • Spring physics uses stiffness, damping, and mass to create elastic, bouncy animations that feel responsive and natural.

Frequently Asked Questions

How is motion value animation different from CSS transitions?

CSS transitions are simpler but less flexible. Motion values give you precise control over physics, velocity, and state-driven updates. Use CSS for simple property changes; use motion values for gesture-driven, physics-based animations.

What decay factor should I use for inertial scrolling?

Typical values range from 0.92–0.98. Lower values (0.92) stop motion faster, higher values (0.98) let it coast longer. Test on your target device and adjust to match native feel.

How do I prevent spring oscillation from being too bouncy?

Increase the damping coefficient. For example, change damping from 0.1 to 0.3 or 0.4. Damping is the "friction" that slows oscillation. Higher damping means less bounce and faster settling.

Can I combine multiple motion values (e.g., position and rotation)?

Yes. Create separate motion values for each property, then apply them to the same element. Update both in the animation loop, and they'll animate in sync.

Should I use a library like Framer Motion or build custom motion values?

For production apps, Framer Motion is worth the dependency. It handles complex scenarios (layout animations, SVG morphing, shared layout animations) and has optimized performance. For simple cases, custom motion values are lightweight and educational.

Further Reading