Skip to content
Animation

Spring — Component Library Guide

Spring-physics animation for React that interpolates outside the render loop, so moving hundreds of elements costs no extra re-renders per frame.

TypeScript MIT
5.5M weekly downloads
29.1k GitHub stars
20.5 kB min+gzip bundle size
v10.1.2 latest version

Figures above measured on from the npm registry (latest version, last-week downloads) and the GitHub API (stars). Not copied from the project's own README, and re-measured rather than edited by hand, so the date tells you exactly how old these numbers are.

How the bundle size was measured. We installed @react-spring/web@10.1.2 on its own, bundled export * from '@react-spring/web' with esbuild (esm, browser, minified, production), marked React external because your app already has it, and gzipped the result. Measured . This is the ceiling, not the typical cost: it imports the entire public surface with no tree-shaking credit. Pull one component from a well-shaken library and you'll pay a fraction of it. We publish the ceiling because it's the number we can reproduce, and reproducing it is the point.

animationspring-physicstransitionsgesturesperformance

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
  • useSpring for one value set, useSprings for many, useTransition for mount and unmount
  • useTrail for 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.

Frequently Asked Questions

What does spring physics give me that a duration and an easing curve don't?

Interruptibility that looks right. A duration-based tween restarted mid-flight either jumps or finishes the old curve first. A spring carries its current velocity into the new target, so a card yanked back while still settling continues from where it actually is. For anything a user can interrupt, that's the whole argument.

Why the animated.div wrapper instead of a plain div?

Because that's what keeps animation off the React render path. An animated component subscribes to the spring value and writes it straight to the DOM node's style each frame. A plain div would need a state update per frame, which means a re-render per frame.

How does it compare to Framer Motion?

Framer Motion has the friendlier API and better layout-animation support, and it's the faster thing to reach for on a marketing page. React Spring is smaller, physics-first, and does better when many elements animate at once or when values feed something other than the DOM. Both are good; they optimise for different problems.

Does it respect prefers-reduced-motion?

Not on its own. Read the media query yourself and set immediate: true on the spring when it matches, which applies the target value with no animation. Wiring it into a shared hook once is the reliable way to stop it being forgotten per component.