react-motion vs react-spring vs react-transition-group vs react-transition-state
React Animation and Transition Libraries Compared
react-motionreact-springreact-transition-groupreact-transition-stateSimilar Packages:

React Animation and Transition Libraries Compared

react-motion, react-spring, react-transition-group, and react-transition-state are libraries used to handle animations and state transitions in React applications. react-spring is a physics-based animation library that succeeds react-motion. react-transition-group is a low-level utility for managing component entry and exit transitions, often paired with CSS. react-transition-state offers a lightweight, hook-based alternative for managing transition states without the overhead of a full transition group. Together, they cover the spectrum from complex physics-driven motion to simple CSS class toggling.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
react-motion021,930-1939 years agoMIT
react-spring029,1017.23 kB897 days agoMIT
react-transition-group010,244244 kB258-BSD-3-Clause
react-transition-state048931.8 kB115 days agoMIT

React Animation and Transition Libraries Compared

When building interactive React applications, motion adds polish and guides user attention. However, choosing the right tool depends on whether you need physics-based interpolation, CSS class management, or simple state tracking. react-motion, react-spring, react-transition-group, and react-transition-state each solve different parts of this puzzle. Let's break down how they work and when to use them.

šŸ›‘ Maintenance Status: Legacy vs Active

react-motion is the predecessor to react-spring.

  • It introduced spring physics to React but is no longer maintained.
  • It lacks modern React features like hooks and has known performance issues with large lists.
// react-motion: Legacy API (Class-based / Render Props)
import { Motion, spring } from 'react-motion';

function Box() {
  return (
    <Motion defaultStyle={{ x: 0 }} style={{ x: spring(100) }}>
      {interpolatingStyle => (
        <div style={{ transform: `translateX(${interpolatingStyle.x}px)` }} />
      )}
    </Motion>
  );
}

react-spring is the active successor.

  • Built by the same creator with a focus on performance and hooks.
  • Supports server-side rendering and tree-shaking better than its predecessor.
// react-spring: Modern Hooks API
import { useSpring, animated } from '@react-spring/web';

function Box() {
  const props = useSpring({ to: { x: 100 }, from: { x: 0 } });
  return <animated.div style={{ transform: props.x.to(x => `translateX(${x}px)`) }} />;
}

react-transition-group is actively maintained by the React community.

  • It is a low-level utility, not a full animation library.
  • It focuses on mounting/unmounting components with CSS classes.
// react-transition-group: CSSTransition
import { CSSTransition } from 'react-transition-group';

function FadeItem({ show, children }) {
  return (
    <CSSTransition in={show} timeout={300} classNames="fade">
      {children}
    </CSSTransition>
  );
}

react-transition-state is a lightweight, modern alternative.

  • It uses hooks to track state without wrapping children in specific components.
  • Ideal for simpler use cases where react-transition-group feels heavy.
// react-transition-state: Hook-based state
import { useTransitionState } from 'react-transition-state';

function FadeItem({ show, children }) {
  const { mounted, transitionState } = useTransitionState(show, { timeout: 300 });
  if (!mounted) return null;
  return <div className={`fade-${transitionState}`}>{children}</div>;
}

šŸŽØ Animation Model: Physics vs Duration

The way you define timing changes drastically between these libraries.

react-spring uses physics.

  • You define tension and friction, not seconds.
  • Animations interrupt naturally without glitching.
// react-spring: Spring config
const props = useSpring({
  from: { opacity: 0 },
  to: { opacity: 1 },
  config: { tension: 280, friction: 60 }
});

react-motion also used physics.

  • Similar config but older implementation.
  • Less optimized for modern React concurrent features.
// react-motion: Spring config
<Motion style={{ opacity: spring(1, { stiffness: 180, damping: 20 }) }}>
  {style => <div style={style} />}
</Motion>

react-transition-group uses time-based CSS.

  • You define a timeout in milliseconds.
  • You write CSS transitions that match that time.
/* react-transition-group: CSS matching timeout */
.fade-enter {
  opacity: 0;
}
.fade-enter-active {
  opacity: 1;
  transition: opacity 300ms;
}

react-transition-state uses time-based state.

  • You define a timeout in milliseconds.
  • It gives you a state string ('entering', 'exiting') to apply classes.
// react-transition-state: State string
const { transitionState } = useTransitionState(show, { timeout: 300 });
// transitionState === 'entering' | 'entered' | 'exiting' | 'exited'

🧩 Component Structure: Wrappers vs Hooks

How you integrate these into your component tree varies significantly.

react-transition-group requires wrapping children.

  • You must use <CSSTransition> or <TransitionGroup>.
  • It clones elements, which can sometimes cause issues with certain component types.
// react-transition-group: Wrapper component
<CSSTransition in={isOpen} timeout={300} classNames="modal">
  <ModalContent />
</CSSTransition>

react-transition-state uses hooks inside the component.

  • No wrapper needed around children.
  • Gives you full control over what renders based on state.
// react-transition-state: Internal hook
function Modal({ isOpen }) {
  const { mounted, transitionState } = useTransitionState(isOpen, { timeout: 300 });
  if (!mounted) return null;
  return <div className={`modal-${transitionState}`}>...</div>;
}

react-spring uses animated wrappers.

  • You wrap DOM nodes with animated.div to bind values.
  • Keeps animation logic declarative within the render.
// react-spring: Animated component
import { animated } from '@react-spring/web';

return <animated.div style={props}>Content</animated.div>;

react-motion uses render props.

  • You pass a function to the <Motion> component.
  • More verbose than hooks.
// react-motion: Render prop
<Motion style={{ x: spring(100) }}>
  {interpolated => <div style={{ left: interpolated.x }} />}
</Motion>

šŸ“¦ Handling Lists and Groups

Animating lists (entering/leaving items) is a common challenge.

react-transition-group provides <TransitionGroup>.

  • Specifically designed to handle multiple children entering/leaving.
  • Manages the mounting logic for you.
// react-transition-group: List handling
import { TransitionGroup, CSSTransition } from 'react-transition-group';

<TransitionGroup>
  {items.map(item => (
    <CSSTransition key={item.id} timeout={300} classNames="list-item">
      <li>{item.text}</li>
    </CSSTransition>
  ))}
</TransitionGroup>

react-spring provides useTransition.

  • Handles list animations with spring physics.
  • Powerful for reordering and complex list effects.
// react-spring: List handling
const transitions = useTransition(items, {
  from: { opacity: 0, transform: 'translateY(-10px)' },
  enter: { opacity: 1, transform: 'translateY(0)' },
  leave: { opacity: 0, transform: 'translateY(-10px)' }
});

react-transition-state requires manual mapping.

  • No built-in group component.
  • You map the hook over items yourself.
// react-transition-state: Manual list handling
{items.map(item => {
  const { mounted } = useTransitionState(item.visible, { timeout: 300 });
  return mounted ? <li key={item.id}>{item.text}</li> : null;
})}

react-motion requires manual handling.

  • No dedicated list helper like TransitionGroup.
  • You manage keys and mounting logic manually.
// react-motion: Manual list handling
{items.map(item => (
  <Motion key={item.id} style={{ opacity: spring(item.visible ? 1 : 0) }}>
    {style => <li style={style}>{item.text}</li>}
  </Motion>
))}

🌱 When Not to Use These

These tools are powerful, but overkill for simple cases.

  • Simple CSS Transitions: If you just need a hover effect, use plain CSS. No library needed.
  • One-off Animations: For a single load-in animation, consider CSS keyframes or the Web Animations API.
  • react-motion: As stated, avoid this in new work. It is legacy technology.

šŸ“Œ Summary Table

Featurereact-springreact-transition-groupreact-transition-statereact-motion
Primary UsePhysics animationsCSS Enter/ExitHook-based StateLegacy Physics
API StyleHooks + ComponentsComponentsHooksRender Props
TimingSprings (Tension/Friction)Duration (ms)Duration (ms)Springs (Stiffness/Damping)
List SupportuseTransition<TransitionGroup>ManualManual
Maintenanceāœ… Activeāœ… Activeāœ… ActiveāŒ Deprecated

šŸ’” The Big Picture

react-spring is your go-to for motion design.

  • Use it when the animation itself is the feature (dragging, bouncing, complex sequences).
  • It replaces react-motion in every way.

react-transition-group is the standard for UI state changes.

  • Use it for modals, dropdowns, and list items where CSS classes drive the look.
  • It is robust and widely understood by React developers.

react-transition-state is the lightweight hook alternative.

  • Use it when you want the logic of react-transition-group without the wrapper components.
  • Great for custom components where you need access to the transition state internally.

react-motion is history.

  • Respect its contribution, but build with react-spring instead.

Final Thought: For most modern apps, a combination of react-spring (for interactive motion) and react-transition-group or react-transition-state (for mounting transitions) provides the best balance of power and simplicity.

How to Choose: react-motion vs react-spring vs react-transition-group vs react-transition-state

  • react-motion:

    Do not choose react-motion for new projects. It is effectively deprecated and unmaintained. The creator moved development to react-spring, which offers better performance, TypeScript support, and a more modern API. Using react-motion today introduces technical debt and potential compatibility issues with modern React versions.

  • react-spring:

    Choose react-spring if you need complex, physics-based animations that feel natural and interactive. It is ideal for drag gestures, springy UI elements, and coordinated animations across multiple properties. It requires a shift in thinking from duration-based to spring-based timing, but offers superior control for high-fidelity motion design.

  • react-transition-group:

    Choose react-transition-group if you need standard, CSS-driven enter/exit transitions (like fading a modal in or out). It is the industry standard for mounting and unmounting components with animation. It is best suited for declarative transitions where you define CSS classes for states like 'enter-active' or 'exit-done'.

  • react-transition-state:

    Choose react-transition-state if you want a lighter, hook-based solution for managing transition states without the complexity of react-transition-group. It is great for simple toggles where you need to know if an element is entering, leaving, or mounted, allowing you to apply classes or styles manually without the overhead of cloning children.

README for react-motion

React-Motion

Build Status npm version Bower version react-motion channel on discord

import {Motion, spring} from 'react-motion';
// In your render...
<Motion defaultStyle={{x: 0}} style={{x: spring(10)}}>
  {value => <div>{value.x}</div>}
</Motion>

Animate a counter from 0 to 10. For more advanced usage, see below.

Install

  • Npm: npm install --save react-motion

  • Bower: do not install with bower install react-motion, it won't work. Use bower install --save https://unpkg.com/react-motion/bower.zip. Or in bower.json:

{
  "dependencies": {
    "react-motion": "https://unpkg.com/react-motion/bower.zip"
  }
}

then include as

<script src="bower_components/react-motion/build/react-motion.js"></script>
  • 1998 Script Tag:
<script src="https://unpkg.com/react-motion/build/react-motion.js"></script>
(Module exposed as `ReactMotion`)

Works with React-Native v0.18+.

Demos

Check the wiki for more!

Try the Demos Locally

git clone https://github.com/chenglou/react-motion.git
cd react-motion
npm install
  • With hot reloading (slow, development version): run npm start.
  • Without hot reloading (faster, production version): run npm run build-demos and open the static demos/demo_name/index.html file directly. Don't forget to use production mode when testing your animation's performance!

To build the repo yourself: npm run prepublish.

What does this library try to solve?

My React-Europe talk

For 95% of use-cases of animating components, we don't have to resort to using hard-coded easing curves and duration. Set up a stiffness and damping for your UI element, and let the magic of physics take care of the rest. This way, you don't have to worry about petty situations such as interrupted animation behavior. It also greatly simplifies the API.

This library also provides an alternative, more powerful API for React's TransitionGroup.

API

Exports:

  • spring
  • Motion
  • StaggeredMotion
  • TransitionMotion
  • presets

Here's the well-annotated public Flow type definition file (you don't have to use Flow with React-motion, but the types help document the API below).

P.S. using TypeScript? Here are the React-motion TypeScript definitions!


Helpers

- spring: (val: number, config?: SpringHelperConfig) => OpaqueConfig

Used in conjunction with the components below. Specifies the how to animate to the destination value, e.g. spring(10, {stiffness: 120, damping: 17}) means "animate to value 10, with a spring of stiffness 120 and damping 17".

  • val: the value.

  • config: optional, for further adjustments. Possible fields:

    • stiffness: optional, defaults to 170.
    • damping: optional, defaults to 26.
    • precision: optional, defaults to 0.01. Specifies both the rounding of the interpolated value and the speed (internal).

    It's normal not to feel how stiffness and damping affect your spring; use Spring Parameters Chooser to get a feeling. Usually, you'd just use the list of tasteful stiffness/damping presets below.

- Presets for {stiffness, damping}

Commonly used spring configurations used like so: spring(10, presets.wobbly) or spring(20, {...presets.gentle, precision: 0.1}). See here.


<Motion />

Usage

<Motion defaultStyle={{x: 0}} style={{x: spring(10)}}>
  {interpolatingStyle => <div style={interpolatingStyle} />}
</Motion>

Props

- style: Style

Required. The Style type is an object that maps to either a number or an OpaqueConfig returned by spring() above. Must keep the same keys throughout component's existence. The meaning of the values:

  • an OpaqueConfig returned from spring(x): interpolate to x.
  • a number x: jump to x, do not interpolate.
- defaultStyle?: PlainStyle

Optional. The PlainStyle type maps to numbers. Defaults to an object with the same keys as style above, whose values are the initial numbers you're interpolating on. Note that during subsequent renders, this prop is ignored. The values will interpolate from the current ones to the destination ones (specified by style).

- children: (interpolatedStyle: PlainStyle) => ReactElement

Required function.

  • interpolatedStyle: the interpolated style object passed back to you. E.g. if you gave style={{x: spring(10), y: spring(20)}}, you'll receive as interpolatedStyle, at a certain time, {x: 5.2, y: 12.1}, which you can then apply on your div or something else.

  • Return: must return one React element to render.

- onRest?: () => void

Optional. The callback that fires when the animation comes to a rest.


<StaggeredMotion />

Animates a collection of (fixed length) items whose values depend on each other, creating a natural, springy, "staggering" effect like so. This is preferred over hard-coding a delay for an array of Motions to achieve a similar (but less natural-looking) effect.

Usage

<StaggeredMotion
  defaultStyles={[{h: 0}, {h: 0}, {h: 0}]}
  styles={prevInterpolatedStyles => prevInterpolatedStyles.map((_, i) => {
    return i === 0
      ? {h: spring(100)}
      : {h: spring(prevInterpolatedStyles[i - 1].h)}
  })}>
  {interpolatingStyles =>
    <div>
      {interpolatingStyles.map((style, i) =>
        <div key={i} style={{border: '1px solid', height: style.h}} />)
      }
    </div>
  }
</StaggeredMotion>

Aka "the current spring's destination value is the interpolating value of the previous spring". Imagine a spring dragging another. Physics, it works!

Props

- styles: (previousInterpolatedStyles: ?Array<PlainStyle>) => Array<Style>

Required function. Don't forget the "s"!

  • previousInterpolatedStyles: the previously interpolating (array of) styles (undefined at first render, unless defaultStyles is provided).

  • Return: must return an array of Styles containing the destination values, e.g. [{x: spring(10)}, {x: spring(20)}].

- defaultStyles?: Array<PlainStyle>

Optional. Similar to Motion's defaultStyle, but an array of them.

- children: (interpolatedStyles: Array<PlainStyle>) => ReactElement

Required function. Similar to Motion's children, but accepts the array of interpolated styles instead, e.g. [{x: 5}, {x: 6.4}, {x: 8.1}]

(No onRest for StaggeredMotion because we haven't found a good semantics for it yet. Voice your support in the issues section.)


<TransitionMotion />

Helps you to do mounting and unmounting animation.

Usage

You have items a, b, c, with their respective style configuration, given to TransitionMotion's styles. In its children function, you're passed the three interpolated styles as params; you map over them and produce three components. All is good.

During next render, you give only a and b, indicating that you want c gone, but that you'd like to animate it reaching value 0, before killing it for good.

Fortunately, TransitionMotion has kept c around and still passes it into the children function param. So when you're mapping over these three interpolated styles, you're still producing three components. It'll keep interpolating, while checking c's current value at every frame. Once c reaches the specified 0, TransitionMotion will remove it for good (from the interpolated styles passed to your children function).

This time, when mapping through the two remaining interpolated styles, you'll produce only two components. c is gone for real.

import createReactClass from 'create-react-class';

const Demo = createReactClass({
  getInitialState() {
    return {
      items: [{key: 'a', size: 10}, {key: 'b', size: 20}, {key: 'c', size: 30}],
    };
  },
  componentDidMount() {
    this.setState({
      items: [{key: 'a', size: 10}, {key: 'b', size: 20}], // remove c.
    });
  },
  willLeave() {
    // triggered when c's gone. Keeping c until its width/height reach 0.
    return {width: spring(0), height: spring(0)};
  },
  render() {
    return (
      <TransitionMotion
        willLeave={this.willLeave}
        styles={this.state.items.map(item => ({
          key: item.key,
          style: {width: item.size, height: item.size},
        }))}>
        {interpolatedStyles =>
          // first render: a, b, c. Second: still a, b, c! Only last one's a, b.
          <div>
            {interpolatedStyles.map(config => {
              return <div key={config.key} style={{...config.style, border: '1px solid'}} />
            })}
          </div>
        }
      </TransitionMotion>
    );
  },
});

Props

First, two type definitions to ease the comprehension.

  • TransitionStyle: an object of the format {key: string, data?: any, style: Style}.

    • key: required. The ID that TransitionMotion uses to track which configuration is which across renders, even when things are reordered. Typically reused as the component key when you map over the interpolated styles.

    • data: optional. Anything you'd like to carry along. This is so that when the previous section example's c disappears, you still get to access c's related data, such as the text to display along with it.

    • style: required. The actual starting style configuration, similar to what you provide for Motion's style. Maps keys to either a number or an OpaqueConfig returned by spring().

  • TransitionPlainStyle: similar to above, except the style field's value is of type PlainStyle, aka an object that maps to numbers.

- styles: Array<TransitionStyle> | (previousInterpolatedStyles: ?Array<TransitionPlainStyle>) => Array<TransitionStyle>

Required. Accepts either:

  • an array of TransitionStyle configs, e.g. [{key: 'a', style: {x: spring(0)}}, {key: 'b', style: {x: spring(10)}}].

  • a function similar to StaggeredMotion, taking the previously interpolating styles (undefined at first call, unless defaultStyles is provided), and returning the previously mentioned array of configs. You can do staggered mounting animation with this.

- defaultStyles?: Array<TransitionPlainStyle>

Optional. Similar to the other components' defaultStyle/defaultStyles.

- children: (interpolatedStyles: Array<TransitionPlainStyle>) => ReactElement

Required function. Similar to other two components' children. Receive back an array similar to what you provided for defaultStyles, only that each style object's number value represent the currently interpolating value.

- willLeave?: (styleThatLeft: TransitionStyle) => ?Style

Optional. Defaults to () => null. The magic sauce property.

  • styleThatLeft: the e.g. {key: ..., data: ..., style: ...} object from the styles array, identified by key, that was present during a previous render, and that is now absent, thus triggering the call to willLeave. Note that the style property is exactly what you passed in styles, and is not interpolated. For example, if you passed a spring for x you will receive an object like {x: {stiffness, damping, val, precision}}.

  • Return: null to indicate you want the TransitionStyle gone immediately. A Style object to indicate you want to reach transition to the specified value(s) before killing the TransitionStyle.

- didLeave?: (styleThatLeft: {key: string, data?: any}) => void

Optional. Defaults to () => {}.

  • styleThatLeft: the {key:..., data:...} that was removed after the finished transition.
- willEnter?: (styleThatEntered: TransitionStyle) => PlainStyle

Optional. Defaults to styleThatEntered => stripStyle(styleThatEntered.style). Where stripStyle turns {x: spring(10), y: spring(20)} into {x: 10, y: 20}.

  • styleThatEntered: similar to willLeave's, except the TransitionStyle represents the object whose key value was absent during the last render, and that is now present.

  • Return: a defaultStyle-like PlainStyle configuration, e.g. {x: 0, y: 0}, that serves as the starting values of the animation. Under this light, the default provided means "a style config that has the same starting values as the destination values".

Note that willEnter and defaultStyles serve different purposes. willEnter only triggers when a previously inexistent TransitionStyle inside styles comes into the new render.

(No onRest for TransitionMotion because we haven't found a good semantics for it yet. Voice your support in the issues section.)


FAQ

  • How do I set the duration of my animation?

Hard-coded duration goes against fluid interfaces. If your animation is interrupted mid-way, you'd get a weird completion animation if you hard-coded the time. That being said, in the demo section there's a great Spring Parameters Chooser for you to have a feel of what spring is appropriate, rather than guessing a duration in the dark.

  • How do I unmount the TransitionMotion container itself?

You don't. Unless you put it in another TransitionMotion...

  • How do I do staggering/chained animation where items animate in one after another?

See StaggeredMotion

  • My ref doesn't work in the children function.

React string refs won't work:

<Motion style={...}>{currentValue => <div ref="stuff" />}</Motion>

This is how React works. Here's the callback ref solution.