Written by David Omotayo✏️
Animations can provide a powerful user experience if they are well executed. However, attempting to create stunning animations with CSS can be nerve-wracking. Many animation libraries promise to simplify the process, but most simply aren’t comprehensive enough for building complex animations.
In this article, we’ll demonstrate how to create scroll animations with Framer Motion, a complete animation library that doesn't require you to be a CSS expert to create beautiful animations.
Jump ahead:
- Prerequisites
- What is Framer Motion?
- What is intersection observer functionality?
- Getting started
- Creating the demo app
- Animating with variants
- Adding scroll reveal functionality
Prerequisites
- Working knowledge of React and its concepts, including Hooks
- Working knowledge of CSS properties such as opacity, transition, and scale
There’s no need to have any prior knowledge of Framer Motion. This article will introduce the library’s basic concepts and build on those in the demonstration portion.
Let’s start with a little background on Framer Motion and intersection observer functionality.
What is Framer Motion?
Framer Motion is an animation library for creating declarative animations in React. It provides production-ready animations and a low-level API to help simplify the process of integrating animations into an application.
Some React animations libraries, like react-transition-group and transition-hook, animate elements with manually configured CSS transitions. Framer Motion takes a different approach, by animating elements under the hood with preconfigured styles.
motion
and uaeAnimation
are two styles that are triggered and controlled by functions exposed by Framer Motion. The motion
function is used to create motion components, and these are the building blocks of Framer Motion.
By prefixing motion
to a regular HTML or SVG element, the element automatically becomes a motion component:
Motion Component
A motion component has access to several props, including the animate
prop. animate
takes in an object with the defined properties of the components to be animated. The properties defined in the object are animated when the component mounts.
What is intersection observer functionality?
Framer Motion animates elements when they mount on the DOM. It doesn't have inbuilt functionality for animating elements based on their scroll position on the viewport. To address this, we’ll need to implement an intersection observer functionality that will prevent an element from mounting until its scroll position is in the viewport.
We can build this functionality from scratch as a custom Hook using the Intersection Observer API. This JavaScript API provides a way to asynchronously observe changes in the intersection of a target element with a top-level document viewport.
According to the documentation, this API registers a callback function that is executed whenever an element we want to monitor enters or exits another element or enters or exits the viewport.
Alternatively, we can use a library that is designed to handle this functionality. This is the approach that we’ll follow in this article. We’ll be using the react-intersection-observer library, which is a React implementation of the intersection observer API. This library provides Hooks and render props that make it easy to track the scroll position of elements on the viewport.
react-intersection-observer is a relatively small package, so there’s no need to worry about the overhead it may add to your project.
Source: bundlephobia.
Now, let's set up a simple React project and install the necessary dependencies.
Getting started
We’ll start by installing React:
npx create-react-app my-app
Next, we’ll install Framer Motion and react-intersection-observer
:
npm i react-intersection-observer framer-motion
Next, we'll set up a demo app and will use Framer Motion and the react-intersection-observer library to identify when the elements are in view and then apply an animation.
Creating the demo app
First, we’ll create a box component (this could be a or card, modal, or anything else) and import it into the main component, App.js
. We’ll animate this main component when it enters the viewport.
/*Box component*/
const Box = () => {
return (
<div className="box">
<h1>Box</h1>
</div>
);
};
/*Main component*/
export default function App() {
return (
<div className="App">
<Box /> /*imported Box component*/ /*imported Box component*/
</div>
);
}
Next, we’ll import everything else that’s required to create animation from the libraries we installed earlier:
-
motion
anduseAnimation
Hooks from Framer Motion -
useEffect
Hook from React -
useInView
Hook from react-intersection-observer
import { motion, useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";
These are the essential Hooks we’ll need to animate our box component. You’ll get an idea of how each Hook works a little later in this tutorial.
Inside our component is a div
element with the className
: box
. In order to animate the box
element, we need to make it a motion component.
We do this by prefixing motion
to the element:
const Box = () => {
return (
<motion.div className="box">
<h1>Box</h1>
</motion.div>
);
};
We can start animating the box
element as is, by simply adding an initial
and animate
prop to the motion
component and directly defining their object values.
<motion.div
animate={{ x: 100 }}
initial={{x: 0}}
className="box"
></motion.div>
For more complex animation, Framer Motion offers a variants feature.
Animating with variants
Variants are a set of predefined objects that let us declaratively define how we want the animation to look. Variants have labels that can be referenced in a motion component to create animations.
Here’s an example of a variant object:
const exampleVariant = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}
Inside this variant object, exampleVariant
, are two properties: visible
and hidden
. Both properties are passed an object as the value. When the element is visible
, we want the opacity
to be 1
; when it is hidden
, we want it to be 0
.
The above variant object can be referenced in a motion component, like so:
<motion.div variants={exampleVariant} />
Next, we’ll create a variant and pass it as a prop to our motion component:
const boxVariant = {
visible: { opacity: 1, scale: 2 },
hidden: { opacity: 0, scale: 0 },
}
In this variant object, boxVariant
, we included a scale
property so that the element will scale up in size when it is visible
and scale down when it is hidden
.
To reference this variant object it in our motion component, we’ll add a variants
prop to the motion component and pass it the variant's label:
<motion.div
variants={boxVariant}
className="box"
/>
Right now, nothing is happening to our motion component; it has access to the variant object, but it doesn't know what to do with it. The motion component needs a way to know when to start and end the animations defined in the variant object.
For this, we pass the initial
and animate
prop to the motion component:
<motion.div
variants={boxVariant}
className="box"
initial="..."
animate="..."
/>
In the above code, the initial
prop defines the behavior of a motion component before it mounts, while the animate
prop is used to define the behavior when it mounts.
Now, we’ll add a fade-in animation effect to the motion component by setting the opacity
of the component to 0
before it mounts and back to 1
when it mounts. The transition
property has a duration value that indicates the animation duration
.
<motion.div
className="box"
initial={{ opacity: 0, transition:{duration: 1}}}
animate={{opacity: 1}}
/>
Since we're using variants, we don't have to explicitly set the values of the initial
and animate
properties.
Instead, we can dynamically set them by referencing the hidden
and visible
properties in the variant object we created earlier:
const boxVariant = {
visible: { opacity: 1, scale: 2 },
hidden: { opacity: 0, scale: 0 },
}
...
<motion.div
variants={boxVariant}
initial="hidden"
animate="visible"
className="box"
/>
The motion component will inherit the values of the variant object’s hidden
and visible
properties and animate accordingly:
Now that we have a working animation for our motion component, the next step is to use the react-intersection-observer library to access the Intersection Observer API and trigger the animation when the component is in view.
Adding scroll reveal animation with useInView
and useAnimation
Hooks
Framer Motion animates elements when they mount, so before we can animate elements based on their scroll position, we need to be able to control when they mount and unmount.
The useAnimation
Hook provides helper methods that let us control the sequence in which our animations occur. For example, we can use the control.start
and control.stop
methods to manually start and stop our animations.
useInView
is a react-intersection-observer Hook that lets us track when a component is visible in the viewport. This Hook gives us access to a ref
, that we can pass into the components we want to watch, and the inView
Boolean, which tells us whether a component is in the viewport.
For example, if we pass ref
to a component as a prop and log inView
to the console, the console will display true
when the component is scrolled into the viewport and false
when it leaves the viewport.
Now, we’ll use the useAnimation
Hook to trigger animations on our motion component with when it enters the viewport.
First, we’ll destructure ref
and inView
from the useInView
Hook, and assign useAnimation
to a variable:
const control = useAnimation()
const [ref, inView] = useInView()
Next, we’ll add ref
to our motion component as a prop and pass the control
variable as a value to the animate
prop:
<motion.div
ref={ref}
variants={boxVariant}
initial="hidden"
animate={control}
className="box"
/>
Finally, we’ll create a useEffect
to call the control.start
method whenever the component we're watching is in view, and pass the control
and inView
variables as the dependencies:
useEffect(() => {
if (inView) {
control.start("visible");
}
}, [control, inView]);
Inside the useEffect
callback function, we perform a conditional check with an if
statement to check if the motion component is in view. If the condition is true
, useEffect
will call the control.start
method with a "visible"
value passed into it. This will trigger the animate
property on our motion component and start the animation.
Now, if we scroll up and down our viewport, the box components will animate when their scroll position enters the viewport:
Notice how the box components only animate the first time they enter the viewport. We can make them animate every time they are in view by adding an else
block to the if
statement in the useEffect
callback function, and calling the control.start
method, but with a "hidden"
value passed into it this time.
else {
control.start("hidden");
}
Now, if we scroll up and down our viewport, the box components will animate each time their scroll position enters the viewport:
Here’s a look at the final code for creating scroll animations with Framer Motion:
import { motion, useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";
const boxVariant = {
visible: { opacity: 1, scale: 1, transition: { duration: 0.5 } },
hidden: { opacity: 0, scale: 0 }
};
const Box = ({ num }) => {
const control = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
control.start("visible");
} else {
control.start("hidden");
}
}, [control, inView]);
return (
<motion.div
className="box"
ref={ref}
variants={boxVariant}
initial="hidden"
animate={control}
>
<h1>Box {num} </h1>
</motion.div>
);
};
export default function App() {
return (
<div className="App">
<Box num={1} />
<Box num={2} />
<Box num={3} />
</div>
);
}
Conclusion
In this article, we introduced the basics of the Framer Motion animation library and demonstrated how to use it to create scroll animations. We discussed how to control animations using the useAnimation
Hook, and how to trigger animations with Intersection Observer API (accessed through the react-intersection-observer library).
This article offers just a glimpse into the extensive range of animations that can be created with Framer Motion. Visit the official docs and see what else you can come up with.
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (1)
thank you!