EmpireUI
Get Pro
← Blog8 min read#react-spring#animation#physics

react-spring: Physics-Based Animations Without the Complexity

react-spring gives you spring physics animations in React without the math. Here's how to use useSpring, useTrail, and useTransition to build buttery UI motion.

developer coding React animation on a dark monitor screen

Why Physics Animations Feel Different

CSS transitions ease-in-out are fine. They get the job done. But they're fake — a predefined curve that has no relationship to how objects actually move in the real world. Spring physics is different: instead of saying "take 300ms to go from A to B", you describe tension and friction, and the animation finds its own duration based on those forces. It ends when it's done, not when the timer says so.

That's why spring animations feel *right* in a way that duration-based ones don't. Think about dragging a drawer open on iOS — it doesn't snap to position after exactly 250ms. It overshoots slightly, recoils, settles. That behavior is what react-spring gives you with basically zero math on your end.

Honestly, most developers I've seen reach for CSS keyframes or even Framer Motion when react-spring would have been the better call. react-spring v9 (released in 2021 and stable ever since) unified the API into hooks that feel natural in a modern React codebase. If you've used useState and useEffect, you'll pick up useSpring in about 15 minutes.

Worth noting: react-spring doesn't touch the DOM via React's reconciler by default. It drives animations through its own animated primitives, which means zero re-renders per frame. Your component tree stays quiet while the animation runs. That's a meaningful performance difference on complex UIs, especially if you're layering motion over something like an aurora-style background.

Getting Started: useSpring in 30 Lines

Install the package — npm install @react-spring/web — and you're ready. No peer dependencies, no config, no build plugin required. The @react-spring/web package is the web-specific entry point; they also ship @react-spring/native and @react-spring/three if you need those.

Here's the simplest useful example: a card that fades in and slides up when a button is pressed.

import { useSpring, animated } from '@react-spring/web'

export function FadeCard() {
  const [visible, setVisible] = React.useState(false)

  const styles = useSpring({
    opacity: visible ? 1 : 0,
    transform: visible ? 'translateY(0px)' : 'translateY(24px)',
    config: { tension: 280, friction: 22 },
  })

  return (
    <>
      <button onClick={() => setVisible(v => !v)}>Toggle</button>
      <animated.div style={styles} className="card">
        Hello, spring
      </animated.div>
    </>
  )
}

The config object is where the physics live. tension controls how aggressively the spring pulls toward the target — higher values mean snappier motion. friction is the damping force — lower friction means more bounce. A tension: 280, friction: 22 combo is a good starting point for UI elements: snappy but not jittery. If you want something bouncier, try tension: 200, friction: 12.

One more thing — notice that animated.div is not a regular div. It's react-spring's proxy element that knows how to receive animated values and apply them directly to the DOM without going through React's diffing. Always wrap with animated.* when you want performant animations. Using a regular div forces React to re-render on every frame, which kills performance on anything more than a simple fade.

Quick aside: react-spring ships its own preset configs via config.gentle, config.wobbly, config.stiff, and config.molasses. They're convenient shortcuts but worth replacing with explicit values once you know what feel you want.

useTrail: Staggering Lists Without a Loop

Staggered list animations — where items enter one after another with a slight delay — used to require either animation-delay hacks in CSS or a setTimeout chain in JS. react-spring has useTrail for exactly this, and it's much cleaner.

import { useTrail, animated } from '@react-spring/web'

const items = ['Design', 'Code', 'Deploy']

export function TrailList() {
  const [open, setOpen] = React.useState(false)

  const trail = useTrail(items.length, {
    opacity: open ? 1 : 0,
    x: open ? 0 : -20,
    from: { opacity: 0, x: -20 },
    config: { mass: 1, tension: 320, friction: 26 },
  })

  return (
    <div onClick={() => setOpen(o => !o)}>
      {trail.map((style, i) => (
        <animated.div key={items[i]} style={style}>
          {items[i]}
        </animated.div>
      ))}
    </div>
  )
}

The x shorthand maps to translateX internally. react-spring handles the CSS transform string construction for you — you just feed it raw numbers. That's one of those quality-of-life details that makes the API genuinely pleasant to use.

In practice, useTrail runs each spring as soon as the previous one reaches roughly 30% completion, creating the cascade effect automatically. You don't set delay values manually. The spacing between items is a natural consequence of the spring physics, which means it scales correctly whether you have 3 items or 30.

This pattern works especially well for navigation menus, card grids, and feature lists — the kind of UI you'd find across Empire UI templates that need to feel polished without over-engineering the motion.

useTransition: Mounting and Unmounting with Style

Fading something in is easy. Fading it out while *also* unmounting it after the animation completes? That's where most animation libraries make you write boilerplate. react-spring's useTransition handles the full mount/unmount lifecycle as a first-class concept.

import { useTransition, animated } from '@react-spring/web'

export function ModalWrapper({ show, children }) {
  const transitions = useTransition(show, {
    from:  { opacity: 0, scale: 0.95 },
    enter: { opacity: 1, scale: 1 },
    leave: { opacity: 0, scale: 0.95 },
    config: { tension: 300, friction: 28 },
  })

  return transitions(
    (style, item) =>
      item && (
        <animated.div style={style} className="modal-overlay">
          {children}
        </animated.div>
      )
  )
}

The callback pattern in transitions() is a bit unusual at first — you're rendering inside a function. But it gives react-spring control over *when* the element is removed from the DOM, which it does only after the leave animation finishes. No setTimeout(cleanup, 300) hacks. No tracking mounted state in a ref.

You can also pass an array to useTransition to animate items entering and leaving a list — like a notification stack or a filter result set. When you add an item to the array, it runs enter. When you remove one, it runs leave and then unmounts. The API handles key tracking internally based on a keys option you provide.

Look, useTransition is probably the hook that sets react-spring apart from CSS animation approaches most clearly. If you've ever tried to animate an element out before removing it using only CSS classes and event listeners, you'll appreciate what this abstraction buys you.

Gesture-Driven Springs with useDrag

react-spring pairs with @use-gesture/react to create drag, pinch, scroll, and hover interactions driven by physics. You install it separately — npm install @use-gesture/react — but the two libraries are designed to work together.

import { useSpring, animated } from '@react-spring/web'
import { useDrag } from '@use-gesture/react'

export function DraggableCard() {
  const [{ x, y }, api] = useSpring(() => ({ x: 0, y: 0 }))

  const bind = useDrag(({ offset: [ox, oy], last }) => {
    api.start({
      x: ox,
      y: oy,
      config: last
        ? { tension: 250, friction: 30 }  // snap back feel
        : { tension: 800, friction: 50 },  // tight follow during drag
    })
  })

  return (
    <animated.div
      {...bind()}
      style={{ x, y, touchAction: 'none' }}
      className="drag-card"
    >
      Drag me
    </animated.div>
  )
}

Notice the config switch between dragging and releasing. While the user is actively dragging, you want high tension so the element tracks the finger tightly — around 800 works well. On release (last: true), you drop the tension to let the physics breathe and the card settle naturally. This two-config approach is what makes draggable UI feel premium.

The touchAction: 'none' CSS is required whenever you're handling drag gestures — it prevents the browser's native scroll behavior from competing with your custom drag handler. Forgetting it causes weird scroll-fighting on mobile, which is a subtle bug that shows up in production and is annoying to track down.

This kind of gesture-to-physics connection is what makes react-spring genuinely different from something like Framer Motion's layout animations. Framer is excellent for declarative choreography. react-spring shines when you need real-time input driving the motion — cards you can fling, drawers you can drag open, sliders that snap. The mental model maps directly to physical interaction.

Performance Tips and Common Mistakes

react-spring's biggest performance win is frame-level animation without React re-renders — but you can accidentally throw that away. The most common mistake is passing animated values as props to non-animated components. Always use animated.div, animated.span, animated.img, etc., or wrap your own components with animated() from react-spring.

// WRONG — triggers React re-render every frame
<div style={{ opacity: springValue.opacity }}>...</div>

// RIGHT — bypasses React's reconciler entirely
<animated.div style={{ opacity: springValue.opacity }}>...</animated.div>

// For custom components:
const AnimatedMyCard = animated(MyCard)
<AnimatedMyCard style={springStyles} />

Second common issue: defining the spring config object inline inside the render function. { tension: 280, friction: 22 } as a literal object gets recreated on every render, which can cause the spring to reset unexpectedly. Define it as a constant outside the component or use config from react-spring's preset exports.

Worth noting: react-spring v9 introduced an imperative API via api.start() and api.stop(). When you need to trigger animations in response to events (scroll position, WebSocket messages, RAF loops), the imperative style is cleaner than toggling state and hoping the declarative form catches up. It's especially useful for scroll-linked animations where you're updating values on every scroll event at up to 120fps.

If you're building something visually intensive — like animating glassmorphism card reveals on scroll or chaining multiple spring sequences — use useChain to sequence multiple springs. It accepts an array of spring refs and a timing array to define when each spring starts relative to the sequence. Fine-grained control without promise chains or nested timeouts.

When to Use react-spring vs. Other Options

react-spring isn't always the right call. For simple hover states, transition in CSS or Tailwind's transition utilities are faster to write and good enough. For complex choreographed page transitions and shared element animations, Framer Motion's AnimatePresence and layoutId are genuinely better. react-spring's sweet spot is physics-driven, gesture-connected, high-performance UI motion.

The library is also a natural fit when you're building design systems or component libraries — things like accordion panels, modal stacks, tooltips with spring-y entrance, and drag-to-sort interfaces. Browse the Empire UI component library for examples of motion-forward components where spring physics pays off over simple CSS transitions.

One more thing — if you're building something with a strong aesthetic identity, like a cyberpunk or vaporwave themed UI where motion is part of the brand, react-spring's configurability is a real advantage. You can tune the exact personality of each spring to match the vibe — a cyberpunk UI might want tight, mechanical springs (high tension, high friction), while vaporwave calls for languid, floaty motion (low tension, barely any friction).

In practice, I've found that teams who try react-spring once and understand the physics model rarely go back to pure duration-based animation. The mental shift from "this takes 300ms" to "this has these physical properties" feels weird for about a day, then clicks permanently. After that, you'll find yourself reaching for it any time you want UI that feels like it has weight.

FAQ

Does react-spring work with Next.js App Router?

Yes, but you need to add 'use client' at the top of any component that uses react-spring hooks, since hooks require a client context. Server components can't use useSpring or any of the other animation hooks.

What's the difference between tension and friction in react-spring?

Tension is how hard the spring pulls toward its target — higher values snap faster. Friction is how much it resists movement — lower friction means more bounce and overshoot. Start with tension: 280, friction: 22 and tune from there.

Can I animate SVG elements with react-spring?

Yes. Use animated.path, animated.circle, animated.rect, etc. All standard SVG elements have animated equivalents, so you can drive stroke-dashoffset, cx/cy positions, and other SVG attributes with spring physics.

Is react-spring still maintained in 2026?

Yes — the v9 API is stable and the library has active community maintenance. It's one of the most downloaded animation libraries in the React ecosystem with over 3 million weekly npm downloads.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Canvas Animations in React: requestAnimationFrame, Particles, PathsAnimation Performance in React: GPU Layers, will-change and the Right Toolsreact-spring Physics Guide: Springs, Trails, Parallax in ReactHTML Canvas Animations in React: Particles, Noise Fields, More