@react-spring/web
@react-spring/web is the browser target of React Spring, a React animation library built around spring physics rather than fixed timelines. Its hooks produce animated values, and its animated HTML and SVG components update those values without sending every frame through React rendering. It handles single-property motion, coordinated lists, enter and leave transitions, gesture-driven values, scroll-linked effects, and imperative animation control.
Choose @react-spring/web for interruptible, interaction-led React motion where spring behavior earns its API and bundle cost. For ordinary fades, exact design-tool timelines, or non-React pages, a smaller CSS or Web Animations approach is easier to own.
Use it if
- You want motion that can change destination mid-flight without hand-tuning a new CSS keyframe timeline
- You need enter and leave animation for dynamic React lists or dialogs through useTransition
- You are building drag, hover, or scroll interactions where animated values must follow continuously changing input
- You want one hook-based API for HTML elements, SVG attributes, numeric values, colors, and transform strings
- You only need a few predictable fades or transforms: CSS transitions and keyframes avoid roughly 20.1 KB gzipped plus five internal runtime packages
- Your designers specify exact durations and easing curves for every motion: React Spring is physics-first, and duration mode exists but gives up the main reason to use it
- You need a small declarative API for mount animations only: useTransition has a distinct configuration and render-function model that is more machinery than libraries such as react-transition-group
- Your test suite cannot accommodate animation scheduling: the official Jest guide has to set the global skipAnimation switch and still advance a tick or wait for the DOM update
- You are not using React on the web: this package has hard React and React DOM peer dependencies, while React Native and React Three Fiber require different React Spring target packages
Setup reality
Install @react-spring/web alongside React and React DOM. Version 10.1.2 accepts React 16.8 through 19, so most current React applications satisfy the peers, and the package ships both ESM and CommonJS entries with TypeScript declarations. The conceptual setup is the bigger cost. Values returned by useSpring are SpringValue objects, not plain numbers, and they belong on animated.div or another animated component. Reading them during render or putting them on a normal DOM element produces confusing results. The object form of useSpring returns only styles, while the function form with a dependency array returns [styles, api]; that overload catches people who copy an imperative example into declarative code. List removal needs useTransition because a component cannot animate after React has already unmounted it. Spring tuning uses mass, tension, and friction, not a duration by default, and easing only applies when duration is configured. Respect reduced-motion preferences with useReducedMotion near the app root, since it changes a global skipAnimation setting. Tests also need scheduling work: the official Jest recipe sets Globals.assign({ skipAnimation: true }) and then waits a tick or advances fake timers. The documentation additionally calls out a Jest ESM resolution workaround for older configurations. In Next.js or another React Server Components framework, the hook-using component must be a client component because it uses React state, effects, and browser animation frames.
Patterns
Animate a value from React stateanimate-state-change
import { animated, useSpring } from '@react-spring/web'
function Panel({ open }) {
const styles = useSpring({
opacity: open ? 1 : 0,
y: open ? 0 : 16,
})
return <animated.div style={styles}>Settings</animated.div>
}Put spring styles on an animated element. A normal div does not know how to subscribe to SpringValue updates.
Start an animation from an eventcontrol-imperatively
import { animated, useSpring } from '@react-spring/web'
function NudgeButton() {
const [styles, api] = useSpring(() => ({ x: 0 }), [])
return (
<animated.button
style={styles}
onClick={() => api.start({ from: { x: 0 }, to: { x: 20 } })}
>
Nudge
</animated.button>
)
}The [styles, api] tuple comes from the function overload with dependencies. Passing a plain object returns styles only.
Tune mass, tension, and frictiontune-spring-physics
const styles = useSpring({
from: { scale: 0.9, opacity: 0 },
to: { scale: 1, opacity: 1 },
config: { mass: 1, tension: 210, friction: 20 },
})
return <animated.div style={styles}>Ready</animated.div>Springs have no fixed duration by default. If the specification requires an exact time, set config.duration and treat it as a duration animation.
Give each animated property different physicsconfigure-per-property
const styles = useSpring({
from: { opacity: 0, y: 30 },
to: { opacity: 1, y: 0 },
config: key =>
key === 'opacity'
? { duration: 180 }
: { tension: 220, friction: 22 },
})Easing has an effect only with duration. A config function receives the animated property key and can mix duration and spring behavior.
Animate list items entering and leavingtransition-list-items
import { animated, useTransition } from '@react-spring/web'
const transitions = useTransition(items, {
keys: item => item.id,
from: { opacity: 0, height: 0 },
enter: { opacity: 1, height: 40 },
leave: { opacity: 0, height: 0 },
})
return transitions((style, item) => (
<animated.div style={style}>{item.label}</animated.div>
))Provide stable keys for object lists. useTransition retains leaving items long enough to animate them out of the DOM.
Create one spring per itemanimate-many-items
import { animated, useSprings } from '@react-spring/web'
const springs = useSprings(people.length, people.map((person, index) => ({
from: { opacity: 0, y: 12 },
to: { opacity: 1, y: 0 },
delay: index * 40,
})))
return springs.map((style, index) => (
<animated.div key={people[index].id} style={style}>
{people[index].name}
</animated.div>
))The count and configuration list must stay aligned. For items that are actually added and removed, useTransition tracks lifecycle more safely.
Make following items trail the firstcreate-trailing-sequence
import { animated, useTrail } from '@react-spring/web'
const trail = useTrail(labels.length, {
from: { opacity: 0, x: -12 },
to: { opacity: 1, x: 0 },
})
return trail.map((style, index) => (
<animated.span key={labels[index]} style={style}>
{labels[index]}
</animated.span>
))useTrail couples each spring to the one before it. Use useSprings when every item needs independent timing or destinations.
Interpolate one spring into a CSS transformderive-transform-value
const { progress } = useSpring({
from: { progress: 0 },
to: { progress: 1 },
})
return (
<animated.div
style={{ transform: progress.to(p => `scale(${0.8 + p * 0.2})`) }}
/>
)Use SpringValue.to for derived display values. Calling get during render reads a snapshot and does not subscribe the element to future frames.
React when an animation settlesrun-rest-callback
const styles = useSpring({
opacity: visible ? 1 : 0,
onRest: result => {
if (result.finished && !result.cancelled) {
console.log('animation settled')
}
},
})onRest can run after interruption as well as completion. Check the result flags before treating it as a completed workflow step.
Honor the operating-system motion preferencerespect-reduced-motion
import { useReducedMotion } from '@react-spring/web'
function App() {
useReducedMotion()
return <Routes />
}The hook synchronizes React Spring's global skipAnimation setting. Call it near the root so every controller follows the same preference.
Drive a progress bar from page scrollanimate-scroll-progress
import { animated, useScroll } from '@react-spring/web'
function ScrollProgress() {
const { scrollYProgress } = useScroll()
return (
<animated.div
style={{ scaleX: scrollYProgress, transformOrigin: 'left' }}
className="progress"
/>
)
}useScroll reads browser scroll state, so this component belongs on the client in frameworks with server components.
Make animation tests deterministicdisable-animation-in-tests
import { Globals } from '@react-spring/web'
beforeEach(() => {
Globals.assign({ skipAnimation: true })
})
afterEach(() => {
Globals.assign({ skipAnimation: false })
})Skipping animation still schedules the React update. Await Testing Library waitFor or advance fake timers before asserting the final style.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| framer-motion | npm | You want a larger but more declarative animation system with layout animation, gestures, and variants |
| react-transition-group | npm | You mainly need enter and exit lifecycle states and prefer to write the CSS yourself |
| @motionone/react | npm | You want a compact Web Animations API based React layer for duration-driven motion |
| gsap | npm | You need timeline-heavy, framework-independent animation with precise sequencing |