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.
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.
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
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` }} />
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
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
Using a library that is no longer maintained introduces security risks and compatibility issues with future React versions.
| Package | Status | React Version Support | Recommendation |
|---|---|---|---|
framer-motion | ✅ Active | 16.8+ (Hooks) | Primary Choice |
react-spring | ✅ Active | 16.8+ (Hooks) | Primary Choice |
react-transition-group | ✅ Active | 16+ | For CSS Transitions |
react-motion | ⚠️ Archived | 15/16 (Class/Render Props) | Avoid |
react-move | ⚠️ Unmaintained | 16+ | Avoid |
react-animations | ⚠️ Legacy | 15+ | Avoid |
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.
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.
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.
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.
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.
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.
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.
Beautiful, data-driven animations for React. Just 3.5kb (gzipped)!
// 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+.
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>
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:
React Move exports just two components:
| Name | Type | Default | Description |
|---|---|---|---|
| data * | array | An array. The data prop is treated as immutable so the nodes will only update if prev.data !== next.data. | |
| keyAccessor * | function | Function that returns a string key given the data and its index. Used to track which nodes are entering, updating and leaving. | |
| interpolation | function | numeric | A function that returns an interpolator given the begin value, end value, attr and namespace. Defaults to numeric interpolation. See docs for more. |
| start * | function | A function that returns the starting state. The function is passed the data and index and must return an object. | |
| enter | function | () => {} | 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. |
| update | function | () => {} | 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. |
| leave | function | () => {} | 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 * | function | A function that receives an array of nodes. |
| Name | Type | Default | Description |
|---|---|---|---|
| show | bool | true | Boolean value that determines if the child should be rendered or not. |
| interpolation | function | numeric | A function that returns an interpolator given the begin value, end value, atrr and namespace. See docs for more. |
| start | union: func object | An object or function that returns an obejct to be used as the starting state. | |
| enter | union: 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. | |
| update | union: 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. | |
| leave | union: 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 * | function | A function that receives the 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:
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'
}
}
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>
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>
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 }
},
]
<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>
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
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
},
}
}
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.
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.
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 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.
We love contributions from the community! Read the contributing info here.
npm installcd docsnpm installnpm startRun these from the root of the repo
npm run lint Lints all files in src and docsnpm run test Runs the test suite locallynpm run test:coverage Get a coverage report in the consolenpm run test:coverage:html Get an HTML coverage report in coverage folderGo to live examples, code and docs!