Written by Paul Akinyemi✏️
Animation in React, and on the web at large, is the process of changing the visual state of the UI elements on a page over time. What do I mean by visual state? Any property of the element that influences how it looks: height, shape, position relative to other elements, etc. The core idea of animation is that you’re changing some visible property of something on the page over time.
There are a few ways to create animations in React, but all of them fall into two broad categories: CSS animations, which change visual state by applying CSS rules; and, JavaScript animations, which use JavaScript to change the properties of the element. In either of those categories, you can either implement the animation from scratch, or you can use a library. On the CSS side, you can compose your animations with CSS rules, or you can use third-party libraries like Animate.css.
If you choose to use JavaScript, you can either write custom code to create animations, or you can use libraries like GSAP or Framer Motion. Each library has its advantages, and each has a different approach for write animations. In this article, we’ll explore Framer Motion, a React animation library created and maintained by the Framer design team.
You’ll learn the core components that underpin all Framer Motion animations, dive into some of the features that make Framer Motion a great tool, discover best practices to get the most out of the library, and put it all into practice with a step-by-step example.
Jump ahead:
- Why Framer Motion for React animations?
- Framer Motion components and APIs
- Implementing animations in a React app using Framer Motion
- Variants in Framer Motion
- Framer Motion page transitions
- Building a drag-and-drop UI with Framer Motion
- Integrating SVG animations
- Best practices for optimizing Framer Motion
To follow along, you’ll need:
- Working knowledge of React, vanilla HTML, CSS, and JS
- Working knowledge of the command line and npm
Why Framer Motion for React animations?
Why should you consider using Framer Motion in your React project? Framer Motion is a fairly popular and actively maintained library, with 19k stars on GitHub, and plenty of resources to support it.
But most importantly, Framer Motion is built around the idea of allowing you to write complex, production-grade animations with as little code as possible. Using Framer Motion is so convenient that you can implement drag-and-drop by adding a single line of code! Framer Motion also greatly simplifies tasks like SVG animation and animating layout shifts.
Framer Motion components and APIs
Framer Motion has an intuitive approach to animation. It provides a set of components that wrap your markup and accept props to allow you to specify what type of animation you want. The core components of Framer Motion are:
- The
motion
component - The
AnimatePresence
component
The motion
component provides the foundation of all animation. It wraps the HTML elements in your React components and animates those elements with state passed to its initial
and animate
props. Below is an example. Take a plain div you might find anywhere on the web:
<div>I have some content here</div>
Let’s assume you wanted this div to fade into the page when it loads. This code is all you need:
<motion.div
initial={{ opacity:0 }}
animate={{ opacity:1 }}
>
I have some content in here
</motion.div>
When the page loads, the div will animate smoothly from transparency to full opacity, gradually fading into the page. In general, when the motion component is mounted, the values specified in the initial
prop are applied to the component, and then the component is animated until it reaches the values specified in the animate
prop.
Next, let’s look at AnimatePresence
. This component works with motion
and is necessary to allow elements you remove from the DOM to show exit animations before they’re removed from the page. AnimatePresence
only works on its direct children that fulfill one of two conditions:
- The child is wrapped with a
motion
component - The child has an element wrapped with a
motion
component as one of its children
The exit animation you want has to be specified by adding the exit
prop to motion
. Here’s an example of AnimatePresence
at work:
<AnimatePresence>
<motion.div
exit={{ x: "-100vh", opacity: 0 }}
>
Watch me go woosh!
</motion.div>
</AnimatePresence>
When the div wrapped by AnimatePresence
is removed from the DOM, instead of just disappearing, it will slide 100vh to the left, fading into transparency as it does so. Only after that will the div be removed from the page. Note that when multiple components are direct children of AnimatePresence
, they each need to have a key
prop with a unique value so AnimatePresence
can keep track of them in the DOM.
Those two components are all you need for many animations, but Framer Motion has features that allow for more complex uses. One of those features is a set of props to a motion
component that allows the component to trigger animations in response to gestures made by the end user, like hovering, tapping, or dragging page elements. These props are referred to as gestures. Here's a quick example that shows the use of the hover gesture:
<motion.div
whileHover={{
opacity: 0
}}
>
Hover over me and I'll disappear!
</motion.div>
The whileHover
prop is the hover gesture. The code above will fade out the div while it’s being hovered over, and return it to its previous state when the mouse leaves it.
Let’s look at one last feature before we try a bigger example. What do you use if you want to tweak aspects of the animation, like adjusting the duration or delay? Framer Motion provides a transition
prop that allows you to specify those. Framer Motion also allows you to choose between different types of animation, like spring animations and tween (easing-based) animations, and the transition
prop allows you to control that. Here’s an example:
<motion.div
initial={{ opacity:0 }}
animate={{ opacity:1 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
I have some content here
</motion.div>
This is the same fade-in animation from earlier, but because of the transition
prop, the animation will wait 0.1 seconds to start and will last for 0.5 seconds.
Implementing animations in a React app using Framer Motion
Let’s use everything we’ve learned to put together a more complex example. At the end of this article, you’ll have built an animated notification tray that looks like this:
Project setup and starter code
Start by navigating to the directory where you want the example to live. Next, open your terminal and create a starter React app using Vite with this command:
npm create vite@latest
Then, answer the prompts like this: Now, cd
into the project you just created, run npm install
, and then run npm run dev
. Your project folder should now look like this: Delete the src/assets
folder and App.css
. Now, write the code for the navigation tray without any animation. Start with the project’s CSS by replacing the contents of index.css
with the following:
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-weight: 400;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background-color: #fff;
color: #111827;
}
header {
height: 4rem;
font-size: 1.1rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
padding: 0 2rem;
}
header > .header__left {
width: 50%;
font-size: 1.5rem;
}
header > .header__right {
width: 50%;
display: flex;
justify-content: flex-end;
align-items: center;
list-style-type: none;
margin: 0;
padding: 0;
}
.header__right > * {
margin: 0 1.5rem;
position: relative;
}
.header__right > .notification__button {
height: 2rem;
width: 2rem;
cursor: pointer;
}
.header__right > .notification__icon {
height: 100%;
width: 100%;
}
.header__right > .image {
border-radius: 50%;
height: 3rem;
width: 3rem;
overflow: hidden;
}
.header__right > .image > img {
height: 100%;
width: 100%;
}
.notification__tray {
border-radius: 6px;
box-shadow: 0px 0px 8px #e5e7eb;
position: fixed;
width: 24rem;
top: 4.5rem;
right: 2rem;
color: rgb(65, 65, 81);
font-size: 0.875rem;
line-height: 1.25rem;
}
.notification__tray > ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.notification__tray li {
padding: 1rem 2rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
}
.notification__tray li:hover {
background-color: #e5e7eb;
color: #111827;
}
.notification__tray li .clear__button {
width: 1.5rem;
height: 1.5rem;
cursor: pointer;
}
.notification__tray li .clear__icon {
width: 100%;
height: 100%;
}
.todo__header {
text-align: center;
}
.todo__container {
list-style-type: none;
}
.todo__item {
border: 1px solid #e5e7eb;
border-radius: 5px;
box-shadow: 0px 0px 8px #e5e7eb;
color: #111827;
margin: 1.5rem auto;
width: 350px;
padding: 1.5rem 2rem;
background-color: #e5e7eb;
}
Next comes the code for the page header. In src
, create a file called Header.jsx
, and fill it with this:
import { useState } from "react";
import NotificationTray from "./NotificationTray";
const initialNotifications = [
"User #20 left you a like!",
"User #45 sent you a friend request",
"Your song has been uploaded!",
"Thanks for signing up!",
];
const Header = () => {
const [showNotifications, setShowNotifications] = useState(false);
const [notificationContent, setNotificationContent] =
useState(initialNotifications);
const handleDeleteNotification = (content) => {
setNotificationContent(
notificationContent.filter((item) => item !== content)
);
};
return (
<header>
<div className="header__left">Brand</div>
<ul className="header__right">
<li
className="notification__button"
onClick={() => {
setShowNotifications(!showNotifications);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="notification__icon"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5"
/>
</svg>
</li>
<li className="image">
<img src="https://www.dummyimage.com/48x48" />
</li>
</ul>
{showNotifications ? (
<NotificationTray
notificationContent={notificationContent}
handleDeleteNotification={handleDeleteNotification}
></NotificationTray>
) : null}
</header>
);
};
export default Header;
For brevity, we won’t go over the starter code in detail, but essentially it does a few things:
- Imports the component for the notification tray
- Creates state for the tray and defines a helper function to remove notifications from the state
- Creates the markup for the header, and conditionally renders the tray
Next, write the code for the notification tray component. Create a file called NotificationTray.jsx
and put this code in it:
const NotificationTray = ({
notificationContent,
handleDeleteNotification,
}) => {
return (
<div className="notification__tray">
<ul>
{notificationContent.map((content) => {
return (
<li key={content}>
<span>{content}</span>
<span
className="clear__button"
onClick={() => {
handleDeleteNotification(content);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="clear__icon"
title="Clear notification"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</span>
</li>
);
})}
</ul>
</div>
);
};
export default NotificationTray;
This code:
- Renders the state of the tray as a
<ul>
, with<li>
s for each notification - Uses the helper function from
Header.jsx
to remove a notification when its clear button is clicked
Finally, render the header in App.jsx
like this:
import Header from "./Header"
function App() {
return (
<>
<Header></Header>
</>
)
}
export default App
All of that covers the code necessary to get the notification tray working properly. If you look at your React app in the browser, you should have a webpage that looks like this:
Adding animation with Framer Motion
We’ll start animating a bell icon. The ringing motion that triggers on hover is created by rotating the SVG icon along the z-axis, in one direction and then the other, and then returning it to normal.
Here’s how: at the top of Header.jsx
, import motion
and AnimatePresence
from Framer Motion:
import {motion, AnimatePresence} from "framer-motion";
Then, add animation to the SVG in Header.jsx
:
<motion.svg
whileHover={{
rotateZ: [0, -20, 20, -20, 20, -20, 20, 0],
transition: { duration: 0.5 },
}}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="notification__icon"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5"
/>
</motion.svg>
Here’s how the SVG changed:
- It’s wrapped with
motion
- It has the
whileHover
gesture as a prop, so the animation only triggers when the SVG is hovered over - Inside the object passed to
whileHover
, you specified a range of values in an array. This array is called a keyframe, and it means that instead of animating to a single value, Framer Motion will animate the SVG to each of the values specified in the keyframe - Inside the object you passed to
transition
, you specified that you want the animation to last half a second
After applying that, you should see the bell ring when you hover over it on your webpage: The next animation you’ll implement is the fade-in on entry animation on the notification tray. Enter Notification.jsx
and import motion
and AnimatePresence
:
import { motion, AnimatePresence } from "framer-motion";
Then, modify the outermost div
like so:
<motion.div
className="notification__tray"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<ul>
.....
</ul>
</motion.div>
All that changed is that the div
became motion.div
, and you set the values of the initial
and animate
props so the div starts with an opacity
of 0 and animates to become fully visible. Repeat this on the <li>
s returned from the map
and add a duration of 0.2 seconds like so:
{
notificationContent.map((content) => {
return (
<motion.li
key={content}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
...
</motion.li>
)
})
}
As an extra touch, let’s animate the exit of each notification. You’ll do this by adding a slide-away animation when an li
is removed from the tray. All you need to do now is wrap the <li>
s with an AnimatePresence
component, and then use the exit
prop to specify what should happen when each <li>
is removed. Let’s see how that works:
<ul>
<AnimatePresence>
{notificationContent.map((content) => {
return (
<motion.li
key={content}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ x: "-12rem", opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
....
</motion.li>
)
})}
</AnimatePresence>
</ul>
The exit
prop says that when the <li>
is removed, it should move 12rem (half the width of the tray) to the left, and then fade away before being unmounted. The layout
prop tells Framer Motion to animate any changes in the element’s position caused by layout shifts. This means that when an <li>
is removed from the tray, instead of its siblings jumping up to fill the space, they’ll smoothly glide into their new positions instead. Take a minute to check it out for yourself.
Your last task for this section is to animate the exit of the tray (when it disappears after you click the bell). The animation you want to apply is the same as the exit animation on the <li>
s: slide left and fade away.
Go back into Header.jsx
and wrap the NotificationTray
component with AnimatePresence
:
<AnimatePresence>
{showNotifications ? (
<NotificationTray
notificationContent={notificationContent}
handleDeleteNotification={handleDeleteNotification}
></NotificationTray>
) : null}
</AnimatePresence>
Then, inside NotificationTray.jsx
, add an exit prop to the outermost div:
<motion.div
className="notification__tray"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, x: "-12rem" }}
>
<ul>...</ul>
</motion.div>
And that completes the basics of our animations! Your tray should now have animations that look like this: Next, you’ll see how to use variants to clean up your code and orchestrate child animations.
Variants in Framer Motion
Variants are a mechanism Framer gives you that serve two purposes:
- They make your code cleaner by allowing you to reduce repetition
- They allow you to coordinate the animations of children with the animations of their parents, which means you can do things like stagger child animations
So, what exactly do variants look like? They’re just objects that hold the objects you would pass to the props to a motion
component. It’s easier to understand when you see them, so here’s an example:
const dummyVariant = {
initial: { opacity: 0 },
animate: { opacity: 1 },
}
The dummyVariant
object holds the objects you’d have passed to the initial
and animate
props. The property names don’t have to match the name of the prop they’ll be assigned to, so the variant would have worked the same if it was simply:
const dummyVariant = {
hide: { opacity: 0 },
show: { opacity: 1 },
}
To use a variant on a motion
component, you have to pass the variant to the variants
prop of your motion component. Then you can pass the props of the motion
component the property names of the objects inside of your variant instead of having to type out the full object. Here’s an example:
><motion.div variants={dummyVariant} initial="hide" animate="show">
Watch me!
</motion.div>
Now that you have a general idea of how variants work, let’s go back and use variants to improve our notification tray. Open NotificationTray.jsx
and add this code just under the import:
const notificationVariants = {
initial: { opacity: 0 },
animate: { opacity: 1, transition: { staggerChildren: 0.15 } },
exit: {
x: "-12rem",
opacity: 0,
transition: {
staggerChildren: 0.15,
staggerDirection: -1,
when: "afterChildren",
},
},
}
This code introduced a few new properties, so let’s examine them. Variants allow you to adjust the timing of animations on child elements relative to the animation on their parents with special transition
props like staggerChildren
. The staggerChildren
prop allows you to delay the animation of each child by a constant difference.
For instance, if staggerChildren
is set to 0.15
, the first child will be delayed by 0
seconds, the second by 0.15
, the third by 0.30
, and so on. staggerDirection
can either be 1
or -1
and allows you to decide whether the stagger should start from the first child or the last one. The when
prop applies to the parent. It defines whether the parent’s animation should start before the children start animating or only start after all the children are done.
In the variant you just defined, you staggered the input and exit animation of each child by 0.15 seconds, and said the stagger on the exit animations should start from the last child. You also said the parent’s exit animation should only start after the exit animations of all its children have ended.
Now that you have a variant defined, the outermost div can be refactored to this:
<motion.div
className="notification__tray"
variants={notificationVariants}
initial="initial"
animate="animate"
exit="exit"
>
<ul>...</ul>
</motion.div>
And the <li>
s can be refactored to this:
<motion.li
key={content}
variants={notificationVariants}
exit={{ x: "-12rem", opacity: 0 }}
transition={{ exit: { duration: 0.1 } }}
layout
>
...
</motion.li>
You might notice that the initial
and animate
props are no longer on the <li>
, but the animation still works. This is due to another feature of variants called propagation. Propagation means when a motion
component and its children are using variants, the values of the initial
and animate
props will flow down from the parent to the children until one of the children defines its own animate
prop.
After putting it all together, your webpage should now look like this: And that’s it! You can find the complete code for this example in the Framer Motion documentation.
Framer Motion page transitions
Framer Motion can also integrate with React Router 6, allowing you to animate routing transitions with AnimatePresence
as pages in your app are mounted and unmounted. Here's an example CodeSandbox from the official docs.
Building a drag-and-drop UI with Framer Motion
Just like everything else, Framer Motion makes implementing drag-and-drop easy. To make an element draggable, first wrap it with a motion component, and then add the drag prop. That’s all you need. As an example, you go from this:
<div>Drag me around!</div>
To this:
<motion.div drag>Drag me around</motion.div>
And that’s it! Doing that will let you drag the div anywhere, including off the screen. That's why Framer Motion provides extra props like dragConstraint
to let you limit the range a component can be dragged to, and dragElastic
moderates how elastic the boundary is.
dragConstraint
accepts either an object with values for top, left, right, and bottom, or a ref to another DOM object. The value of dragElastic
ranges from 0
, meaning the boundaries aren’t elastic at all, to 1
, where the boundaries are as elastic as possible. Here’s an example:
<motion.div
drag
dragConstraints={{
top: -50,
left: -50,
right: 50,
bottom: 50,
}}
dragElastic={0.3}
>
Drag me around
</motion.div>
Now, the div
can only be dragged in a region 50px in any direction, and the boundaries of the region are slightly elastic.
Integrating SVG animations
Framer also makes animating SVGs a breeze, by allowing you to animate the pathLength
, pathSpacing
, and pathOffset
properties of those SVGs. Here’s an example that uses the same bell icon we used in our header bar. Animate the pathLength
of the SVG in Header.js
by updating it like this:
<motion.svg
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
whileHover={{
rotateZ: [0, -20, 20, -20, 20, -20, 20, 0],
transition: { duration: 0.5 },
}}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="notification__icon"
>
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2 }}
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5"
/>
</motion.svg>
On page load, the SVG should animate like this:
Best practices for optimizing Framer Motion
Framer Motion is a great tool, but like any tool, it can be misused. Here are a few rules of thumb to optimize performance when using Framer Motion:
- Use the
layoutGroup
component andlayoutId
prop for shared element animations - Leverage hardware acceleration by animating
transform
andopacity
properties whenever possible - Where possible, wrap multiple elements within a parent component and animate the parent to reduce the number of animations
- If you have animations that trigger frequently (e.g., on scroll or mouse movement), consider using debounce or throttle techniques to limit the number of animation updates and improve performance
- Use Framer Motion’s hooks instead of rewriting the functionality yourself
Following these practices will help you optimize performance and create smoother animations with Framer Motion in React.
Conclusion
Web animation is the process of changing the visual state of UI elements over time. Web animation utilizes one of two approaches: CSS or JavaScript. Framer Motion is a popular, well-supported JavaScript-based animation library for React applications that simplifies the process of implementing complex animations. In this article, we used Framer Motion to build an animated notification tray, animate an SVG, and implement drag-and-drop.
I hope you enjoyed this article. Happy coding!
Get setup with LogRocket's modern React error tracking in minutes:
1.Visit https://logrocket.com/signup/ to get an app ID.
2.Install LogRocket via NPM or script tag. LogRocket.init()
must be called client-side, not server-side.
NPM:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Script Tag:
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
3.(Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (1)
Amazing resource!
It's good for understanding the "lower level" of framer but i would have pushed more on the "higher level" side by explaining hooks like useAnimate or usePresence.