react-move vs framer-motion vs react-animations vs react-motion vs react-spring vs react-transition-group
Selecting React Animation Libraries for Production Systems
react-moveframer-motionreact-animationsreact-motionreact-springreact-transition-groupSimilar Packages:

Selecting React Animation Libraries for Production Systems

This comparison evaluates six popular React animation libraries, ranging from modern physics-based engines to legacy CSS transition wrappers. framer-motion and react-spring represent the current standard for declarative, high-performance animations. react-transition-group remains the low-level primitive for CSS-based class transitions. react-motion, react-move, and react-animations are older solutions that are largely considered legacy or unmaintained in modern ecosystems. Understanding their architectural differences helps teams avoid technical debt while selecting the right tool for interaction design.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
react-move84,3396,580-285 years agoMIT
framer-motion031,1684.68 MB176a day agoMIT
react-animations0---8 years agoMIT
react-motion021,734-1938 years agoMIT
react-spring029,0548.36 kB1346 months agoMIT
react-transition-group010,257244 kB258-BSD-3-Clause

React Animation Libraries: Architecture and API Comparison

Choosing an animation library in React is not just about picking a visual effect — it is about selecting an engine that fits your application's architecture. Some libraries use physics to calculate values, while others rely on CSS classes or keyframes. This guide breaks down how six popular packages handle motion, interaction, and lifecycle events.

⚙️ Core Animation Model: Physics vs CSS vs Keyframes

The underlying engine determines how smooth your animations feel and how much control you have.

framer-motion uses a declarative model backed by a powerful animation engine that supports both CSS transitions and spring physics.

// framer-motion: Declarative animate prop
import { motion } from "framer-motion";

<motion.div
  animate={{ x: 100, opacity: 1 }}
  transition={{ type: "spring" }}
/>

react-spring is built entirely on physics. Every animation is a spring that interpolates values over time.

// react-spring: Hook-based physics
import { useSpring, animated } from "@react-spring/web";

const props = useSpring({ to: { x: 100, opacity: 1 }, from: { x: 0, opacity: 0 } });
<animated.div style={props} />

react-transition-group does not calculate values. It toggles CSS classes at specific lifecycle moments.

// react-transition-group: CSS class toggling
import { CSSTransition } from "react-transition-group";

<CSSTransition in={show} timeout={300} classNames="fade">
  <div /> {/* CSS handles .fade-enter-active */}
</CSSTransition>

react-motion uses a render prop to inject interpolated values based on spring physics.

// react-motion: Render prop injection
import { Motion, spring } from "react-motion";

<Motion style={{ x: spring(100) }}>
  {({ x }) => <div style={{ transform: `translateX(${x}px)` }} />}
</Motion>

react-move also uses a render prop pattern similar to react-motion but with a different configuration API.

// react-move: Render prop with state
import { Move } from "react-move";

<Move data={{ x: 100 }}>
  {({ data }) => <div style={{ transform: `translateX(${data.x}px)` }} />}
</Move>

react-animations provides predefined keyframe objects to be used with CSS-in-JS libraries.

// react-animations: Keyframe injection
import { fadeIn } from "react-animations";
import styled from "styled-components";

const Div = styled.div` animation: 1s ${fadeIn}`;
<div /> // Uses CSS @keyframes generated by library

🚪 Handling Mount and Unmount: Enter/Exit Transitions

Animating elements as they appear or disappear requires tracking state changes outside the normal render cycle.

framer-motion uses the AnimatePresence component to detect removal from the tree.

// framer-motion: AnimatePresence wrapper
import { AnimatePresence, motion } from "framer-motion";

<AnimatePresence>
  {show && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>

react-spring uses the useTransition hook to manage item lifecycle and interpolation.

// react-spring: useTransition hook
import { useTransition } from "@react-spring/web";

const transitions = useTransition(show, { from: { opacity: 0 }, to: { opacity: 1 } });

react-transition-group is specifically designed for this. The in prop triggers the CSS classes.

// react-transition-group: in prop control
import { Transition } from "react-transition-group";

<Transition in={show} timeout={300}>
  {(state) => <div className={`item ${state}`} />}
</Transition>

react-motion requires manual handling or external state logic as it does not have a built-in presence system.

// react-motion: Manual visibility check
import { Motion, spring } from "react-motion";

{show && <Motion style={{ opacity: spring(1) }}>{({ opacity }) => <div />}</Motion>}

react-move similar to react-motion, relies on the parent to manage the existence of the component.

// react-move: Conditional rendering
import { Move } from "react-move";

{show && <Move data={{ opacity: 1 }}>{({ data }) => <div />}</Move>}

react-animations does not handle mount/unmount logic. It only styles the element once mounted.

// react-animations: No lifecycle support
import { fadeIn } from "react-animations";

// Must be combined with react-transition-group or similar for exit logic
<div style={{ animation: `${fadeIn} 1s` }} />

🖱️ Interactive State: Hover, Tap, and Drag

Modern apps require motion to respond to user input instantly without complex event listeners.

framer-motion has built-in props for common interactions like whileHover and drag.

// framer-motion: Built-in interaction props
<motion.button whileHover={{ scale: 1.1 }} drag />

react-spring requires using hooks like useSprings or combining with useGesture for interactions.

// react-spring: Manual event handlers
const [props, api] = useSpring(() => ({ scale: 1 }));
<div onMouseEnter={() => api({ scale: 1.1 })} style={props} />

react-transition-group does not support interaction states. It is strictly for lifecycle transitions.

// react-transition-group: No interaction support
// Must use standard CSS :hover or React state
<div className="button" /> // CSS handles :hover

react-motion requires manual event listeners to update spring values.

// react-motion: Manual state update
const [hover, setHover] = useState(0);
<Motion style={{ scale: spring(hover) }}>{({ scale }) => <div />}</Motion>

react-move similar to react-motion, needs manual event wiring to change data values.

// react-move: Manual data update
const [data, setData] = useState({ scale: 1 });
<Move data={data}>{({ data }) => <div />}</Move>

react-animations relies entirely on CSS pseudo-classes for interaction.

// react-animations: CSS only
const Button = styled.button` &:hover { animation: ${pulse} 1s}`;
<Button /> // No JS interaction logic

📐 Layout Animations: Size and Position Changes

Animating layout shifts (like expanding a card or reordering a list) is notoriously difficult in CSS.

framer-motion features layout prop that automatically animates size and position changes.

// framer-motion: Automatic layout animation
<motion.div layout onClick={() => setIsExpanded(!isExpanded)} />

react-spring requires manual measurement of dimensions to interpolate values correctly.

// react-spring: Manual measurement
const { height } = useSpring({ height: isOpen ? 200 : 0 });
<animated.div style={{ height }} />

react-transition-group cannot animate layout changes dynamically without complex CSS transitions.

// react-transition-group: CSS max-height trick
// Requires knowing final height in advance for CSS
<div className={"transition-max-height " + (isOpen ? "open" : "")} />

react-motion can animate values but does not auto-detect layout changes.

// react-motion: Manual value interpolation
<Motion style={{ h: spring(isOpen ? 200 : 0) }}>{({ h }) => <div style={{ height: h }} />}</Motion>

react-move similar to react-motion, requires explicit target values for layout properties.

// react-move: Explicit target data
<Move data={{ height: isOpen ? 200 : 0 }}>{({ data }) => <div style={{ height: data.height }} />}</Move>

react-animations does not support dynamic layout animation. It is for fixed keyframes only.

// react-animations: No dynamic layout support
// Cannot animate to unknown heights with keyframes
<div /> // Static styles only

🛠️ Maintenance & Ecosystem Status

Using a library that is no longer maintained introduces security risks and compatibility issues with future React versions.

PackageStatusReact Version SupportRecommendation
framer-motion✅ Active16.8+ (Hooks)Primary Choice
react-spring✅ Active16.8+ (Hooks)Primary Choice
react-transition-group✅ Active16+For CSS Transitions
react-motion⚠️ Archived15/16 (Class/Render Props)Avoid
react-move⚠️ Unmaintained16+Avoid
react-animations⚠️ Legacy15+Avoid

💡 Final Recommendation

For new projects, framer-motion is the safest bet. It handles complex layout shifts and gestures with minimal code. If you need pure physics control, react-spring is the expert choice. Use react-transition-group only for simple CSS class swaps. Avoid react-motion, react-move, and react-animations as they rely on older patterns and lack active support.

Final Thought: Animation libraries are infrastructure. Choosing a maintained one ensures your UI remains smooth as React evolves.

How to Choose: react-move vs framer-motion vs react-animations vs react-motion vs react-spring vs react-transition-group

  • react-move:

    Avoid react-move for new projects as it is a predecessor to react-spring and is no longer actively developed. It shares similar physics concepts but lacks the modern API improvements and community support of its successor. Migrating to react-spring is recommended for any project currently using this.

  • framer-motion:

    Choose framer-motion for most modern applications requiring complex interactions, layout animations, and gesture support. It offers the best balance of performance and developer experience with a declarative API. It is ideal for dashboards, interactive prototypes, and production apps needing polished motion without deep physics knowledge.

  • react-animations:

    Avoid react-animations for new projects as it is a legacy library focused on predefined keyframes rather than dynamic state. It does not integrate well with modern React hooks or server-side rendering patterns. Use CSS modules or modern animation libraries instead for keyframe-based effects.

  • react-motion:

    Avoid react-motion for new projects as it is effectively unmaintained and relies on older React patterns like render props. It was pioneering for physics animations but has been superseded by more performant and ergonomic libraries. Only consider it for maintaining legacy codebases that already depend on it.

  • react-spring:

    Choose react-spring if your design relies heavily on natural, physics-based movement like springs and damping. It provides granular control over interpolation and is excellent for data visualizations or games. It is suitable when you need a physics engine that integrates tightly with React hooks.

  • react-transition-group:

    Choose react-transition-group for simple mount/unmount transitions controlled by CSS classes. It is the lightest option and works well when you only need to fade or slide elements in and out without complex value interpolation. It is best for basic lists, modals, or route transitions where CSS handles the heavy lifting.

README for react-move

React Table Logo

React-Move

Beautiful, data-driven animations for React. Just 3.5kb (gzipped)!

Documentation and Examples

npm version npm downloads license

Features

  • Animate HTML, SVG & React-Native
  • Fine-grained control of delay, duration and easing
  • Animation lifecycle events: start, interrupt, end
  • Custom tweening functions
  • Awesome documentation and lots of examples
  • Supports TypeScript

Installation

// React ^16.3.0
npm install react-move

// React ^0.14.9 || ^15.3.0 || ^16.0.0
npm install react-move@^5.0.0

Note: The API for React Move 5.x and 6.x is exactly the same. The 5.x version just includes react-lifecycles-compat to make the library work with earlier versions of React. This adds a little to the bundle so use 6.x if you're using React 16.3+.

Upgrading from React Move 2.x and 3.x

The API for React Move has been essentially stable since the 2.0 version. The 4.0 version of React Move introduced a change that broke the hard dependency on d3-interpolate and introduced the interpolation prop. The current version of React Move will by default only do numeric interpolation and apply easing functions. If you only need to do numeric interpolation you don't need to do anything. Just upgrade and done.

To get the same interpolation found in React Move 2.x and 3.x which includes support for colors, paths and SVG transforms do this:

Install d3-interpolate:

npm install d3-interpolate

Then in your app:

import { NodeGroup } from 'react-move'
import { interpolate, interpolateTransformSvg } from 'd3-interpolate'

...
<NodeGroup
  data={this.state.data}
  keyAccessor={(d) => d.name}

  start={(data, index) => ({
    ...
  })}

  enter={(data, index) => ([ // An array
    ...
  ])}

  update={(data) => ({
    ...
  })}

  leave={() => ({
    ...
  })}

  interpolation ={(begValue, endValue, attr) => { // pass as prop
    if (attr === 'transform') {
      return interpolateTransformSvg(begValue, endValue)
    }

    return interpolate(begValue, endValue)
  }}
>
  ...children
</NodeGroup>

Demos

Documentation

The docs below are for version 6.x.x of React-Move.

Older versions:

The API for NodeGroup and Animate have not changed except for the interpolationxw prop, but if you want to refer back:

Getting Started

React Move exports just two components:

  • NodeGroup - If you have an array of items that enter, update and leave
  • Animate - If you have a singe item that enters, updates and leaves

< NodeGroup />

Component Props

NameTypeDefaultDescription
data *arrayAn array. The data prop is treated as immutable so the nodes will only update if prev.data !== next.data.
keyAccessor *functionFunction that returns a string key given the data and its index. Used to track which nodes are entering, updating and leaving.
interpolationfunctionnumericA function that returns an interpolator given the begin value, end value, attr and namespace. Defaults to numeric interpolation. See docs for more.
start *functionA function that returns the starting state. The function is passed the data and index and must return an object.
enterfunction() => {}A function that returns an object or array of objects describing how the state should transform on enter. The function is passed the data and index.
updatefunction() => {}A function that returns an object or array of objects describing how the state should transform on update. The function is passed the data and index.
leavefunction() => {}A function that returns an object or array of objects describing how the state should transform on leave. The function is passed the data and index.
children *functionA function that receives an array of nodes.

< Animate />

Component Props

NameTypeDefaultDescription
showbooltrueBoolean value that determines if the child should be rendered or not.
interpolationfunctionnumericA function that returns an interpolator given the begin value, end value, atrr and namespace. See docs for more.
startunion:
 func
 object
An object or function that returns an obejct to be used as the starting state.
enterunion:
 func
 array
 object
An object, array of objects, or function that returns an object or array of objects describing how the state should transform on enter.
updateunion:
 func
 array
 object
An object, array of objects, or function that returns an object or array of objects describing how the state should transform on update. Note: although not required, in most cases it make sense to specify an update prop to handle interrupted enter and leave transitions.
leaveunion:
 func
 array
 object
An object, array of objects, or function that returns an object or array of objects describing how the state should transform on leave.
children *functionA function that receives the state.

Starting state

Before looking at the components it might be good to look at starting state. You are going to be asked to define starting states for each item in your NodeGroup and Animate components. This is a key concept and probably the most error prone for developers working with React Move. The starting state for each item is always an object with string or number leaves. The leaf keys are referred to as "attrs" as in "attribute." There are also "namespaces" which are a purely organizational concept.

Two rules to live by for starting states:

  • Don't use the strings "timing" or "events" as an attr or namespace.
  • There should never be an array anywhere in your object.

Example starting state:

// GOOD
{
  attr1: 100,
  attr2: 200,
  attr3: '#dadada'
}

// BAD
{
  attr1: [100], // NO ARRAYS
  attr2: 200,
  attr3: '#dadada'
}

A more concrete example might be:

{
  opacity: 0.1,
  x: 200,
  y: 100,
  color: '#dadada'
}

You can add "namespaces" to help organize your state:

{
  attr1: 100,
  attr2: 200,
  attr3: '#ddaabb',
  namespace1: {
    attr1: 100,
    attr2: 200
  }
}

Or something like:

{
  namespace1: {
    attr1: 100,
    attr2: 200
  },
  namespace2: {
    attr1: 100,
    attr2: 200
  }
}

You might use namespaces like so:

{
  inner: {
    x: 100,
    y: 150,
    color: '#545454'
  },
  outer: {
    x: 300,
    y: 350,
    color: '#3e3e3e'
  }
}

Starting state in NodeGroup

In NodeGroup you are working with an array of items and you pass a start prop (a function) that receives the data item and its index. The start prop will be called when that data item (identified by its key) enters. Note it could leave and come back and that prop will be called again. Immediately after the starting state is set your enter transition (optional) is called allowing you to transform that state.

<NodeGroup
  data={data} // an array (required)
  keyAccessor={item => item.name} // function to get the key of each object (required)
  start={(item, index) => ({ // returns the starting state of node (required)
    ...
  })}
>
  {(nodes) => (
    ...
      {nodes.map(({ key, data, state }) => {
        ...
      })}
    ...
  )}
</NodeGroup>

Starting state in Animate

In Animate you are animating a single item and pass a start prop that is an object or a function. The start prop will be called when that the item enters. Note it could leave and come back by toggling the show prop. Immediately after the starting state is set your enter transition (optional) is called allowing you to transform that state.

<Animate
  start={{ // object or function
    ...
  }}
>
  {state => (
    ...
  )}
</Animate>

Transitioning state

You return a config object or an array of config objects in your enter, update and leave props functions for both NodeGroup and Animate. Instead of simply returning the next state these objects describe how to transform the state. Each config object can specify its own duration, delay, easing and events independently.

There are two special keys you can use: timing and events. Both are optional. Timing and events are covered in more detail below.

If you aren't transitioning anything then it wouldn't make sense to be using NodeGroup. That said, it's convenient to be able to set a key to value when a node enters, updates or leaves without transitioning. To support this you can return four different types of values to specify how you want to transform the state.

  • string or number: Set the key to the value immediately with no transition. Ignores all timing values.

  • array [value]: Transition from the key's current value to the specified value. Value is a string or number.

  • array [value, value]: Transition from the first value to the second value. Each value is a string or number.

  • function: Function will be used as a custom tween function.

Example config object:

{
  attr1: [200],
  attr2: 300,
  attr3: ['#dadada']
  timing: { duration: 300, delay: 100 }
}

Using namespaces:

{
  attr1: [100],
  attr3: '#ddaabb',
  namespace1: {
    attr1: [300],
    attr2: 200
  },
  timing: { duration: 300, delay: 100 }
}

To have different timing for some keys use an array of config objects:

[
  {
    attr1: [200, 500],
    timing: { duration: 300, delay: 100 }
  },
  {
    attr2: 300, // this item, not wrapped in an array, will be set immediately, so which object it's in doesn't matter
    attr3: ['#dadada']
    timing: { duration: 600 }
  },
]

Example Transitions in NodeGroup

<NodeGroup
  data={this.state.data}
  keyAccessor={(d) => d.name}

  start={(data, index) => ({
    opacity: 1e-6,
    x: 1e-6,
    fill: 'green',
    width: scale.bandwidth(),
  })}

  enter={(data, index) => ({
    opacity: [0.5], // transition opacity on enter
    x: [scale(data.name)], // transition x on on enter
    timing: { duration: 1500 }, // timing for transitions
  })}

  update={(data) => ({
    ...
  })}

  leave={() => ({
    ...
  })}
>
  {(nodes) => (
    ...
  )}
</NodeGroup>

Using an array of config objects:

import { easeQuadInOut } from 'd3-ease';

...

<NodeGroup
  data={this.state.data}
  keyAccessor={(d) => d.name}

  start={(data, index) => ({
    opacity: 1e-6,
    x: 1e-6,
    fill: 'green',
    width: scale.bandwidth(),
  })}

  enter={(data, index) => ([ // An array
    {
      opacity: [0.5], // transition opacity on enter
      timing: { duration: 1000 }, // timing for transition
    },
    {
      x: [scale(data.name)], // transition x on on enter
      timing: { delay: 750, duration: 1500, ease: easeQuadInOut }, // timing for transition
    },
  ])}

  update={(data) => ({
    ...
  })}

  leave={() => ({
    ...
  })}
>
  {(nodes) => (
    ...
  )}
</NodeGroup>

Timing

If there's no timing key in your object you'll get the timing defaults. You can specify just the things you want to override on your timing key.

Here's the timing defaults...

const defaultTiming = {
  delay: 0,
  duration: 250,
  ease: easeLinear
};

For the ease key, just provide the function. You can use any easing function, like those from d3-ease...

List of ease functions exported from d3-ease

Events

You can add events on your config objects. You can pass a function that will run when the transition starts, is interrupted (an update to the data occurs) or ends.

Using Events:

{
  attr1: [100],
  attr3: '#ddaabb',
  namespace1: {
    attr1: [300],
    attr2: 200
  },
  timing: { duration: 300, delay: 100 },
  events: {
    start: () => {
      ..do stuff - use an arrow function to keep the context of the outer component
    },
    interrupt: () => {
      ..do stuff - use an arrow function to keep the context of the outer component
    },
    end: () => {
      ..do stuff - use an arrow function to keep the context of the outer component
    },
  }
}

Interpolation

You can wire your components in react-move to handle different types of interpolation using the interpolation prop in both NodeGroup and Animate. The code for interpolating strings or SVG paths can be bulky and, in many cases, it's not needed so by default components only handle numeric interpolation.

Your interpolation prop is a function that should avoid a lot of logic and computation. It will get called at high frequency when transitions fire in your components. You get the begin and end values and what the attribute name (string) is. You will also get the namespace string (less common) if you are using them in your state. See the sections on starting states and transitions for more on attrs and namespaces.

Cadillac Interpolation - Depends on d3-interpolate

To wire up a full service interpolation that will interpolate colors, paths, numbers and SVG transforms you can use a setup like this:

npm install react-move d3-interpolate

Then in your app:

import { NodeGroup, Animate } from 'react-move'
import { interpolate, interpolateTransformSvg } from 'd3-interpolate'

...
<NodeGroup
  data={this.state.data}
  keyAccessor={(d) => d.name}

  start={(data, index) => ({
    ...
  })}

  enter={(data, index) => ([ // An array
    ...
  ])}

  update={(data) => ({
    ...
  })}

  leave={() => ({
    ...
  })}

  interpolation ={(begValue, endValue, attr, namespace) => { // pass as prop
    if (attr === 'transform') {
      return interpolateTransformSvg(begValue, endValue)
    }

    return interpolate(begValue, endValue)
  }}
>
  ...children
</NodeGroup>

This setup mimics how d3.js works for selecting interpolators and will not force you to think too much about the values your are using. For example, if you use colors (in any format) they will be recognized and interpolated correctly. The interpolate function exported from d3-interpolate does a great job of guessing what you're trying to do and handles it for you but it also includes a lot of code (e.g. d3-color) that may not be needed for your project.

Numeric Interpolation Only - Default - No dependencies

To do numeric interpolation you don't need to do anything in your components. The default numeric interpolator looks like this:

// The default interpolator used in NodeGroup and Animate

const numeric = (beg, end) => {
  const a = +beg;
  const b = +end - a;

  return function(t) {
    return a + b * t;
  };
};

React-Move vs React-Motion

  • React-move allows you to define your animations using durations, delays and ease functions. In react-motion you use spring configurations to define your animations.

  • React-move is designed to easily plugin interpolation for strings, numbers, colors, SVG paths and SVG transforms. With react-motion you can only interpolate numbers so you have to do a bit more work use colors, paths, etc.

  • In react-move you can define different animations for entering, updating and leaving with the ability to specify delay, duration and ease on each individual key. React-motion allows you to define a spring configuration for each key in the "style" object.

  • React-move has lifecycle events on its transitions. You can pass a function to be called on transition start, interrupt or end. React-motion has an "onRest" prop that fires a callback when the animation stops (just the Motion component not TransitionMotion or StaggeredMotion).

  • React-move also allows you to pass your own custom tween functions. It's all springs in react-motion.

Contributing

We love contributions from the community! Read the contributing info here.

Run the repo locally

  • Fork this repo
  • npm install
  • cd docs
  • npm install
  • npm start

Scripts

Run these from the root of the repo

  • npm run lint Lints all files in src and docs
  • npm run test Runs the test suite locally
  • npm run test:coverage Get a coverage report in the console
  • npm run test:coverage:html Get an HTML coverage report in coverage folder

Go to live examples, code and docs!