DEV Community

Cover image for React scroll animations with Framer Motion
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

React scroll animations with Framer Motion

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

  • 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.

Bundle size of react-intersection-observer 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
Enter fullscreen mode Exit fullscreen mode

Next, we’ll install Framer Motion and react-intersection-observer:

npm i react-intersection-observer framer-motion
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, we’ll import everything else that’s required to create animation from the libraries we installed earlier:

  • motion and useAnimation 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";
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 },
}
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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 },
}
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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="..."
/>
Enter fullscreen mode Exit fullscreen mode

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}}
/>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

The motion component will inherit the values of the variant object’s hidden and visible properties and animate accordingly:

Two Boxes Animate at Once

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.

False and True Outputs

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()
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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:

Boxes Animate While Scrolling

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");
    }
Enter fullscreen mode Exit fullscreen mode

Now, if we scroll up and down our viewport, the box components will animate each time their scroll position enters the viewport:

Boxes Animate Into 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 signup

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.

Oldest comments (1)

Collapse
 
daveb4r profile image
Juan David Pineda

thank you!