Editor’s note: This article was updated by Chizaram Ken in January 2026 to remove outdated libraries and add a hands-on performance and bundle-size benchmark comparing popular animation stacks (including native CSS and Tailwind CSS Motion).
For a React frontend developer, implementing animations on webpages is an integral part of your daily work, from animating text or images to complex 3D animations. Animation can help improve the overall user experience of a React application. But with so many libraries available, it’s hard to know which one to choose. Your animations might be hurting your performance more than you think.
In this article, we’ll compare the top seven React animation libraries and evaluate each for popularity, developer experience, readability, documentation, and bundle size to help you choose the right library for your next React project. We compared them based on bundle size, developer experience (DX), and performance.
Also, at the end of this article, we tested nine popular approaches to find the best animation stack for 2026. This benchmark includes a couple of CSS-first approaches (native CSS and TailwindCSS Motion) alongside the libraries, since they’re common production alternatives for UI animation.
A quick snapshot of all the libraries we explored:
Let’s get into it!
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
React Spring is a modern animation library that is based on spring physics. It’s highly flexible and covers most animations needed for a user interface. Drawing inspiration from React Motion, React Spring inherits ease of use, while also borrowing some powerful performance attributes from React-Animated-CSS.
To see React Spring in action, install the library by using one of the commands below:
npm i @react-spring/web yarn add @react-spring/web
Next, add the code below to create an animated Hello React Spring text:
import { useSpring, animated } from "@react-spring/web";
import { useState } from "react";
function ReactSpring() {
// State to keep track of the animation toggle
const [state, toggle] = useState(true);
// Define the animation using the useSpring hook
const { x } = useSpring({
from: { x: 0 }, // Starting value of the animated property
x: state ? 1 : 0, // Ending value of the animated property based on the state
config: { duration: 1000 }, // Configuration for the animation, specifying the duration
});
// Return the animated component
return (
<div onClick={() => toggle(!state)}>
<animated.div
style={{
opacity: x.to({ range: [0, 1], output: [0.3, 1] }),
transform: x
.to({
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
output: [1, 0.97, 0.9, 1.1, 0.9, 1.1, 1.03, 1],
})
.to((x) => `scale(${x})`),
}}
>
Hello React Spring
</animated.div>
</div>
);
}
export default ReactSpring;
When you click on the div, the above code animates both the opacity and scale of the contents. To understand better, let’s break it down step by step.
Within our component, state is used to toggle the animation. When the div is clicked, the toggle function is called, changing state from true to false, or vice versa. This triggers the animation due to the change in the x value provided to the useSpring Hook.
The useSpring Hook is used to define the animation:
from: Defines the starting values of the animated properties. In this case, we are animating the x property, starting from 0.x: This is the property that will be animated. It is dependent on the state variable, so when state is true, x will be 1, and when state is false, x will be 0.config: Specifies the configuration for the animation. In this example, the animation will have a duration of 1000ms.The animated.div component is used to wrap the contents that we want to animate. It uses the animated wrapper from React Spring, allowing it to animate the styles applied to it. The animation itself is controlled by the style prop of the animated.div component. The style object is defined using the x value obtained from the useSpring Hook.
Two CSS properties are animated in this example:
opacity: The opacity of the content is interpolated from 0.3 to 1 based on the x value.transform: The scale of the content is interpolated based on the x value. The x.to function maps specific input ranges to corresponding output values, effectively defining how the scale changes during the animation.Motion is a popular React animation library that makes creating animations easy. It boasts a simplified API that abstracts the complexities behind animations and allows developers to create animations with ease. Even better: it has support for server-side rendering, gestures, and CSS variables.
To install Motion, run one of the following two commands in your terminal:
yarn add framer-motion npm install framer-motion
Next, add the following code block to add cool, yet simple animation to a square-shaped element:
import { useState } from "react";
import { motion } from "motion/react";
import "./framer-motion.css"; // Import any CSS file if required
function FramerMotion() {
// State to keep track of the animation toggle
const [isActive, setIsActive] = useState(false);
// Return the animated component
return (
<motion.div
className="box"
onClick={() => setIsActive(!isActive)} // Toggle the 'isActive' state on click
animate={{
rotate: isActive ? [0, 90, 180, 270] : [270, 180, 90, 0], // Rotate the element based on 'isActive' state
borderRadius: isActive ? [0, 20, 50] : [50, 20, 0], // Change border radius based on 'isActive' state
}}
></motion.div>
);
}
export default FramerMotion;
When you click on the div, the above code toggles the animation, rotating it in a circular motion and changing its border radius, creating a smooth and visually appealing effect.
Within our component, isActive is used to toggle the animation. When the div is clicked, the onClick event handler is called, which toggles the value of isActive between true and false. The animation is controlled using the animate prop of the motion.div component from Motion. The animate prop takes an object with properties that describe the animations to be applied to the element.
Then, the rotate property animates the rotation of the element. When isActive is true, the element will rotate from zero degrees to 90 degrees, then to 180 degrees, and finally to 270 degrees. When isActive is false, the element will rotate in the reverse order, from 270 degrees to 180 degrees, then to 90 degrees, and finally back to zero degrees.
The borderRadius property animates the border radius of the element. When isActive is true, the border radius will change from zero to 20 and then to 50. When isActive is false, the border radius will change in the reverse order, from 50 to 20, and finally back to zero.
motion component’s features.React Transition Group is a library that provides a set of components to help manage the animation of elements when they are added to or removed from the React component tree. It is commonly used in combination with React for creating smooth and seamless transitions for elements as they enter or exit the DOM.
Note: This library is no longer maintained by the authors.
To install the library, run one of the following commands in your terminal:
npm i react-transition-group yarn add react-transition-group
Next, add this code block to create a basic fade animation using React Transition Group:
import { useState } from "react";
import { Transition } from "react-transition-group";
function ReactTransitionGroup() {
const [show, setShow] = useState(true);
const handleToggle = () => {
setShow(!show);
};
const duration = 300; // Animation duration in milliseconds
// Custom styles for the enter animation
const transitionStylesEnter = {
entering: { opacity: 0 },
entered: { opacity: 1 },
};
// Custom styles for the exit animation
const transitionStylesExit = {
exiting: { opacity: 1 },
exited: { opacity: 0 },
};
return (
<div>
<button onClick={handleToggle}>Toggle Fade</button>
<Transition in={show} timeout={duration}>
{(state) => (
<div
className="fade-element"
style={{
...transitionStylesEnter[state],
...transitionStylesExit[state],
transition: `opacity ${duration}ms`, // Apply the custom animation duration
}}
>
Hello, this is a fade animation!
</div>
)}
</Transition>
</div>
);
}
export default ReactTransitionGroup;
When you click the "Toggle Fade" button, the element will fade in or fade out smoothly, depending on the current value of the show state. The Transition component handles the custom animation logic using the provided styles and lifecycle Hooks. To better understand, let’s break it down step by step.
Within our component, we define show to control the visibility of the fading element. The handleToggle function toggles the show state when the "Toggle Fade" button is clicked. Then, we set the duration variable to control the animation duration for both enter and exit transitions. In this example, the duration is set to 300 milliseconds.
Next, we define two objects, transitionStylesEnter and transitionStylesExit, to specify the custom styles for the enter and exit animations, respectively. These styles are applied based on the current state of the transition using the state argument provided by the render prop function inside the Transition component.
Inside the return statement, we use the Transition component from React Transition Group.
The Transition component takes two main props: in and timeout. The in props is a Boolean value that determines whether the element should be shown or hidden. In this case, it depends on the value of the show state. Meanwhile, the timeout prop displays the duration of the animation in milliseconds. In this example, we set it to the value of the duration variable (300 milliseconds).
The Transition component uses a render prop pattern, where the child function receives the current state of the transition as an argument. The state value can be one of the following strings: "entering", "entered", "exiting", or "exited". These states represent the different stages of the enter and exit transitions. Then, inside the render prop function, we render the fading element (<div className="fade-element">) with inline styles.
The state argument is used to apply the corresponding custom styles that we defined earlier for the enter and exit animations. Finally, the transition property is set to apply the custom animation duration for the opacity transition.
React Transition Group also comes with support for TypeScript, which can be installed using the command: npm i @types/react-transition-group.
React Move is a library designed for creating beautiful and data-driven animations by leveraging the animation capabilities of D3.js. It was designed to support TypeScript out of the box and also supports custom tweening functions.
Tweening is short for “inbetweening,” the process of generating images for frame-by-frame animation between keyframes. React Move also features lifecycle events in its transitions, as well as allowing developers to pass on custom tweens in their animations.
To install React Move, run one of the following two commands in your terminal:
npm install react-move yarn add react-move
Next, add this code block to create animated bars using React Move:
import { useState, useEffect } from "react";
import { Animate } from "react-move";
import "./AnimatedBars.css"; // Import the CSS file with the styles
// Define an array of colors for the bars
const colors = ["#236997", "#52aaeb", "#a75e07", "#f4a22d", "#f95b3aff"];
// Define an array of data values for the bars' heights
const data = [10, 26, 18, 14, 32];
function AnimatedBars() {
// Initialize the state for the index of the data array
const [index, setIndex] = useState(0);
// Use the useEffect hook to set up the interval for updating the index
useEffect(() => {
// Set an interval to update the index every 2 seconds (2000 milliseconds)
const intervalId = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % data.length);
}, 2000);
// Clean up the interval when the component unmounts
return () => {
clearInterval(intervalId);
};
}, []);
// Render the animated bars using react-move
return (
<div className="App">
{/* Create an SVG element to draw the bars */}
<svg version="1.1" viewBox="0 0 240 135" width="100vw">
{/* Group the bars under a 'g' element */}
<g>
{/* Loop through the data array to create animated bars */}
{data.map((value, i) => (
// Use the Animate component to animate the bars' heights
<Animate
key={i}
start={{ height: 0 }} // Initial state of the animation (height starts from 0)
enter={{ height: [value], timing: { duration: 500 } }} // Target state when a new data value is added
update={{
height: [data[(i + index) % data.length]], // Target state when the index state changes
timing: { duration: 500 },
}}
>
{/* Render a rect element for each bar */}
{(state) => (
<rect
x={60 + 30 * i} // X position of the bar
y={115 - state.height} // Y position of the bar (height changes based on state)
width={24} // Width of the bar
height={state.height} // Height of the bar (changes based on state)
fill={colors[i % colors.length]} // Fill color of the bar (based on the colors array)
/>
)}
</Animate>
))}
</g>
</svg>
</div>
);
}
export default AnimatedBars;
The code above renders a set of animated bars using the React Move library, with changing height and color every two seconds. To understand better, let’s break it down.
First, we define an array of colors and data to be used to style the bars and determine their heights. Meanwhile, the index state variable will be used to update the bars. Then, the useEffect() Hook sets up an interval to update the index state every two seconds. This will trigger the animation and update the bars based on the data array.
To render the animated bars, we group the bars using a <g> element inside the <svg> element. Then, we loop through the data array and create an Animate component for each value. For each Animate component, we specify the start, enter, and update configurations for the animation:
start: The initial state of the animation. The height starts from zero enter: The target state of the animation when a new data value is added. The height is set to the corresponding value from the data array update: The target state of the animation when the index state changes. Height is updated to the corresponding value from the data array using the index state
Then, inside the Animate component, we define the animation and render a <rect> element for each data value:
<rect> elements represent the bars x, y, width, and fill attributes are set to style each bar based on its index and data value
The Remotion React library was created in 2021 to create animations using HTML, CSS, and JavaScript. With Remotion, you can create videos as well as play and review them on your browser while using React components and compositions.
Remotion also allows developers to scale video animation and production using server-side rendering and parametrization. Parametrization is the process of finding equations of curves and surfaces using equations, which can be very useful in creating meshes for videos. You can initialize a new Remotion video by running either of the commands below in your terminal:
npm init video yarn create video
A video is a function of images over time. If you change content every frame, you’ll end up with an animation. To create one using Remotion, add the following lines of code:
import { useVideoConfig } from "remotion";
export const MyVideo = () => {
const { fps, durationInFrames, width, height } = useVideoConfig();
const opacity =frame >= 30 ? 1 : frame / 30;
return (
<div
style={{
flex: 1,
textAlign: "center",
fontSize: "9em",
opacity: opacity,
}}
>
This {width}px x {height}px video is {durationInFrames / fps} seconds long.
</div>
);
};
In the code above, we added animation properties using opacity. We also added a video animation using the useVideoConfig() Hook. When creating a video animation, we’d need the width, height, durationInFrames, and fps parameters in Remotion. You can read more about them here.
Anime.js is a lightweight JavaScript animation library with a simple, yet powerful API. It makes animating CSS properties, SVG elements, DOM attributes, and JavaScript objects a breeze.
To install Anime.js, run one of the following commands:
npm install animejs yarn add animejs
To create a cool SVG animation, add the lines of code below:
import { useEffect, useRef } from "react";
import anime from "animejs";
function Anime() {
const circleRef = useRef(null);
const animateCircle = () => {
// Get the DOM element to animate
const circle = circleRef.current;
// Use AnimeJS to create the animation
anime({
targets: circle,
cx: anime.random(10, 500), // Random x position between 10 and 500
cy: anime.random(10, 500), // Random y position between 10 and 500
duration: 2000, // Animation duration in milliseconds
easing: "easeInOutQuad", // Easing function for smooth animation
complete: animateCircle, // Repeat the animation infinitely
});
};
useEffect(() => {
animateCircle();
}, []);
return (
<svg width="500" height="500">
<circle ref={circleRef} r="30" fill="blue" />
</svg>
);
}
export default Anime;
In the code above, a circle moves randomly on the screen within the specified coordinate range and keeps looping infinitely. To understand better, let’s break it down.
Inside the component, a useRef Hook is used to create a reference called circleRef, which is used to get a reference to the SVG circle element that needs to be animated.
Meanwhile, the animateCircle function is responsible for animating the SVG circle element. Inside the function, the DOM element to animate is obtained using the circleRef.current. Then, Anime.js is used to create an animation using the anime() function. The targets option specifies the circle element to animate, and the cx and cy properties are animated with random values between 10 and 500, creating a random movement of the circle.
Then, the duration option sets the animation duration to 2000 milliseconds (two seconds), while the easing option specifies the easing function used for smooth animation. Finally, the complete option sets the animateCircle function to be called again when the animation is completed, creating an infinite loop. The r attribute sets the radius of the circle and the fill attribute sets the circle’s color.
GSAP (GreenSock Animation Platform) is a robust and widely-used animation library for creating high-performance animations and interactive web experiences. It has been a popular choice among developers for many years. Known for its speed, flexibility, and smoothness, GSAP empowers developers to craft seamless animations with ease.
You can install GSAP with one of the commands below:
npm install gsap yarn add gsap
To create a cool scroll-triggered animation, add the following code:
import { useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export default function Scroll() {
const main = useRef();
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
const boxes = self.selector('.box');
gsap.from(boxes, {
x: -500, // Start from left (-100px)
opacity: 0,
duration: 1,
stagger: 0.2, // Add stagger effect for sequential animation
scrollTrigger: {
trigger: main.current,
start: 'top 80%', // Adjust the start position for the effect
end: 'bottom 20%', // Adjust the end position for the effect
scrub: true,
},
});
}, main); // <- Scope!
return () => ctx.revert(); // <- Cleanup!
}, []);
return (
<div>
<section className="section flex-center column">
<h2>Scroll down to see the animation!!</h2>
</section>
<div className="section flex-center column" ref={main}>
<div className="box">Box 1</div>
<div className="box">Box 2</div>
<div className="box">Box 3</div>
<div className="box">Box 4</div>
<div className="box">Box 5</div>
</div>
<section className="section"></section>
</div>
);
}
On page scroll, the boxes will come from the left side of the screen with a stagger effect, and their opacity will increase, creating a smooth animation.
ScrollTrigger, which is imported from GSAP’s ScrollTrigger plugin, enables animations based on scroll positions.
gsap.registerPlugin(ScrollTrigger); is used to register the plugin with GSAP.
Then, const main = useRef(); creates a ref called main to reference the main container.
To set up the animation, gsap.context() is used to create a context for managing animations scoped to the main container. Then, gsap.from() is used to create the animation. The boxes element is selected using self.selector('.box'), and the animation targets each box element, starting from -500 units to the left (x: -500) and with opacity: 0.
The animation duration is set to 1 second, and stagger: 0.2 is used to add a stagger effect for sequential animations among the boxes. Then, scrollTrigger specifies the trigger options for the animation, which will be triggered based on the scroll position.
start: 'top 80%' specifies that the animation starts when the top of the main container is at 80% of the viewport’s height.
Meanwhile, end: 'bottom 20%' specifies that the animation ends when the bottom of the main container is at 20% of the viewport’s height.
Finally, scrub: true enables the scrub feature, which changes the animation progress based on scroll position. The animation will control the x position and opacity as specified in the animation setup.
Animating with some libraries makes you feel like you need an extra degree in designing. And we’re not designers; we’re developers, hence why this Tailwind animation plugin was created. It’s becoming a popular choice for simple, easy-going animations in React.
To install TailwindCSS Motion, run this in your projects:
npm i -D tailwindcss-motion
And go ahead to add this to your tailwind.config.js :
// tailwind.config.js
export default {
content: [...],
theme: {
extend: {...},
},
plugins: [require('tailwindcss-motion')],
};
Or you can use ESM:
import tailwindcssMotion from "tailwindcss-motion";
/** @type {import('tailwindcss').Config} */
export default {
content: [...],
theme: {
extend: {},
},
plugins: [tailwindcssMotion],
};
Now, let’s create a scroll-triggered card animation that fades in and slides up as you scroll:
import { useEffect, useRef, useState } from "react";
function ScrollCards() {
const [isVisible, setIsVisible] = useState({});
const cardRefs = useRef([]);
useEffect(() => {
const observers = cardRefs.current.map((card, index) => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible((prev) => ({ ...prev, [index]: true }));
}
},
{ threshold: 0.1 }
);
if (card) observer.observe(card);
return observer;
});
return () => observers.forEach((observer) => observer.disconnect());
}, []);
const cards = [
{ title: "Feature One", description: "Build faster with Tailwind utilities" },
{ title: "Feature Two", description: "Animate without writing keyframes" },
{ title: "Feature Three", description: "Pure CSS, zero JavaScript overhead" },
];
return (
<div className="min-h-screen bg-gray-50 py-20">
<div className="max-w-4xl mx-auto px-4 space-y-8">
{cards.map((card, index) => (
<div
key={index}
ref={(el) => (cardRefs.current[index] = el)}
className={`
bg-white p-8 rounded-lg shadow-lg
${
isVisible[index]
? "motion-preset-slide-up-lg motion-duration-700"
: "opacity-0 translate-y-12"
}
`}
style={{ transitionDelay: `${index * 150}ms` }}
>
<h3 className="text-2xl font-bold mb-3">{card.title}</h3>
<p className="text-gray-600">{card.description}</p>
</div>
))}
</div>
</div>
);
}
export default ScrollCards;
When you scroll down the page, the cards will appear one by one with a smooth slide-up and fade-in effect. The animation is staggered, creating a polished sequential reveal.
The code uses the Intersection Observer API to detect when each card enters the viewport. Once a card becomes visible, we apply Tailwind CSS Motion’s motion-preset-slide-up-lg utility class, which handles the entire animation with pure CSS. The motion-duration-700 class sets the animation duration to 700 milliseconds.
The beauty of this approach is in its simplicity. Instead of defining custom @keyframes or importing heavy JavaScript libraries, we’re using utility classes that compile down to optimized CSS. The motion-preset-slide-up-lg preset combines translation, opacity, and timing into a single class.
The transitionDelay inline style creates the stagger effect, making each subsequent card animate slightly after the previous one. This adds a professional touch without any complex timing calculations.
Tailwind CSS Motion provides several built-in presets like motion-preset-fade, motion-preset-slide-up, motion-preset-bounce, and more. You can also compose custom animations using granular utilities like motion-translate-x-in-25, motion-opacity-in-0, motion-rotate-in-90, and motion-scale-in-50 for complete control over every animation dimension.
To get a fair comparison, we implemented the same multi-layer parallax scrolling effect with each library. The animation requirements were:
We are evaluating these libraries on these criteria:
These were our contenders:
Here’s why we selected these six:
We also included native CSS as a baseline to demonstrate that modern CSS (with scroll-timeline, transforms, and transitions) can handle many animation needs without any library overhead.
After implementing the parallax component and running a production build, here are the results:
| Library | Bundle Size (Gzipped) | Node Modules | Implementation LOC | Scroll Performance | DX Score | Best For |
|---|---|---|---|---|---|---|
| Native CSS | 0 KB | 0 KB | 322 lines | ⭐⭐⭐⭐⭐ Excellent | 7/10 | Zero-dependency sites |
| Tailwindcss Motion | ~5 KB (CSS) | 492 KB | 128 lines | ⭐⭐⭐⭐⭐ Excellent | 9/10 | Tailwind-based projects |
| React Spring | ~45 KB | 1.0 MB | 213 lines | ⭐⭐⭐⭐ Smooth | 7/10 | Physics-based motion |
| Anime.js | ~52 KB | 1.9 MB | 258 lines | ⭐⭐⭐⭐ Excellent | 7/10 | SVG/Timeline sequences |
| GSAP | ~78 KB | 6.2 MB | 309 lines | ⭐⭐⭐ Good | 7/10 | Professional scrolling |
| Framer Motion | ~85 KB | 3.0 MB | 188 lines | ⭐⭐⭐⭐ Smooth | 10/10 | Complex interactions |
Total Combined Bundle: 560.49 KB (192.77 KB gzipped) – includes React Router and all libraries.
Note: Remotion was excluded from parallax testing as it’s designed for programmatic video creation, not real-time web interactions.
Winner: Tailwindcss Motion (5KB, pure CSS) for simple animations; Motion (85KB, great DX) for complex scroll interactions; GSAP (78KB, pro-grade) when you need bulletproof performance.
If you’d like to migrate to any of these stacks, here are lists of things you must consider:
tailwindcss-motion for basic UI transitionsframer-motion for parallax/scroll effectsReact.lazy())useScroll() + useTransform() for parallax layersvite-bundle-visualizer)Here are some performance tips that will impact your use of these libraries for animations:
React.lazy() and Suspense.transform over top/left: Hardware-accelerated.From our comparison, Tailwindcss Motion will be our go-to for simple animations with decent developer experience, while Motion will be more patronised for complex scroll interactions. GSAP remains a professional choice for me when it comes to advanced requirements.
In terms of popularity, Anime.js leads GitHub stars (65.8k) and React Transition Group tops npm downloads (14M), though note that React Transition Group is no longer actively maintained.
Something to remember is that image optimization impacts performance more than library choice. Many of these libraries are customizable and include great built-in features and changes. Hopefully, going through the pros and cons, you can choose the right library for your next React application.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Animate SVGs with pure CSS: hamburger toggles, spinners, line-draw effects, and new scroll-driven animations, plus tooling tips and fallbacks.

Tailwind CSS is more popular than ever. This guide breaks down v4’s biggest changes, real-world usage, migration paths, and where it fits in the AI future.

AI agents fan out work across multiple LLM calls and services. Task queues add retries, ordering, and context preservation to keep these workflows reliable.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the January 21st issue.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now