TL;DR: React Spring animates with spring physics rather than durations, and writes values straight to the DOM instead of through React state. That makes interrupted animations behave and keeps frame cost flat as element counts grow. Docs: react-spring.dev.
What is React Spring?
React Spring is an animation library built on springs: you describe where a value should end up, and a mass-tension-friction model decides how it gets there. There's no duration and no easing curve to pick.
The second idea matters as much as the first. Spring values live outside React's state, and animated.* components subscribe to them directly. The component renders once; every frame after that is a style write on an existing DOM node.
When to use it
It fits interactions the user can interrupt or drive: drags, swipes, pull-to-refresh, anything gesture-driven. It also fits lists where many items animate together, because the per-frame cost stays outside React.
It's a heavier tool than you need for a hover state or a fade-in. A CSS transition handles those at zero JavaScript cost, and reaching for a library there is how bundles grow without anything getting better.
Key Features
- Spring physics with mass, tension and friction, plus named presets
- Values held outside React state, applied per frame to the DOM node
useSpringfor one value set,useSpringsfor many,useTransitionfor mount and unmountuseTrailfor staggered sequences that follow one leader- Interpolation from one animated value into several derived outputs
- Platform packages for web, native, three and konva sharing one core
Installation
npm install @react-spring/web
The web package is the one you want in a browser app. Installing plain react-spring pulls the meta-package.
A Spring You Can Interrupt
import { useSpring, animated } from '@react-spring/web';
import { useState } from 'react';
export function Panel({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
const style = useSpring({
height: open ? 240 : 0,
opacity: open ? 1 : 0,
config: { tension: 210, friction: 26 },
});
return (
<>
<button onClick={() => setOpen(o => !o)}>
{open ? 'Hide' : 'Show'} details
</button>
<animated.div style={{ overflow: 'hidden', ...style }}>
{children}
</animated.div>
</>
);
}
Click twice quickly and the panel reverses from its current height rather than snapping.
Animating a List
import { useTransition, animated } from '@react-spring/web';
export function Toasts({ items }: { items: { id: string; text: string }[] }) {
const transitions = useTransition(items, {
keys: item => item.id,
from: { opacity: 0, transform: 'translateY(-12px)' },
enter: { opacity: 1, transform: 'translateY(0px)' },
leave: { opacity: 0, transform: 'translateY(-12px)' },
});
return transitions((style, item) => (
<animated.div style={style} role="status">{item.text}</animated.div>
));
}
useTransition holds leaving items in the tree until their spring finishes, which is the part that's tedious to write by hand.
Respecting Reduced Motion
import { useSpring } from '@react-spring/web';
function useReducedMotion(): boolean {
return typeof window !== 'undefined'
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
export function useGentleSpring(to: Record<string, number>) {
const reduce = useReducedMotion();
return useSpring({ to, immediate: reduce });
}
immediate: true jumps to the target without animating. Wrapping it once means nobody has to remember it per component.
TypeScript Tips
useSpring infers its value type from the object you pass, so annotate only where you build the target dynamically. When you interpolate, the callback argument is typed from the source value:
const { x } = useSpring({ x: open ? 1 : 0 });
const shadow = x.to(v => `0 ${v * 8}px ${v * 24}px rgba(0,0,0,0.18)`);
Common Gotchas
Forgetting animated is the classic one. Spread a spring style onto a plain div and you get an object where a string belongs, and nothing moves.
The second is animating layout properties. Springing height or top runs layout every frame. transform and opacity stay on the compositor, so prefer them wherever the design allows.
The third is treating tension and friction as magic numbers. Start from a preset, change one at a time, and check on a low-end device before deciding a config feels right.