DEV Community

Cover image for Using Framer Motion to make page transitions in React
Sam Piggott
Sam Piggott

Posted on

Using Framer Motion to make page transitions in React

In this short tutorial, we'll be learning how we can use the AnimatePresence component provided by Framer Motion to create our very own page transitions when navigating between pages in React!

Animation Example

We'll be using React Router for this example - but this same principle should work for other router implementations, too (have tested with the Next.js router and it worked with no issues!)

Watch the Video Tutorial:

Video Tutorial

You can also download the source code from the link above, too!

Installing the required packages

First of all, let's install the required packages into our React project. We'll need React Router for navigation, and Framer Motion for the transition behaviour.

yarn add react-router-dom framer-motion
Enter fullscreen mode Exit fullscreen mode

Adding a BrowserRouter to our app

Next, in the top-level component where ReactDOM.Render is called (for me, index.tsx), we'll want to wrap our app's container in an instance of <Router> from react-router.

First, we'll add the necessary imports to index.tsx...

import { BrowserRouter as Router } from 'react-router-dom';
Enter fullscreen mode Exit fullscreen mode

Then, we'll wrap our top-level component in an instance of this router.

ReactDOM.render(
  <React.StrictMode>
    <Router> // <-- Adding this...
      <App />
    </Router> // <-- ...and this!
  </React.StrictMode>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

Building a simple switch

Next, we'll need to make some changes to the component immediately beneath the top-level component (in our example, the App component).

Inside the App component, we'll set up a basic routing system for our app. In this example, we're only going to be navigating between two simple pages - so we just need a <Switch> and two <Route> components to get us going.

So let's import those in our App.tsx file first...

import { Switch, Route } from 'react-router-dom';
Enter fullscreen mode Exit fullscreen mode

Then below, we'll return our simple switch from our App component. For clarity, Page1 and Page2 components can be any valid React component.

const App = () => {
  return (
    <Switch>
      <Route path="/page1">
        <Page1 />
      </Route>
      <Route path="/page2">
        <Page2 />
      </Route>
    </Switch>
  );
};
Enter fullscreen mode Exit fullscreen mode

Adding AnimatePresence from Framer Motion

Now, it's time to add the animation! First, we'll wrap our <Switch> in the <AnimatePresence> component from Framer Motion. Let's import that first...

import { AnimatePresence } from 'framer-motion';
Enter fullscreen mode Exit fullscreen mode

Then, let's wrap our app in that new component.

const App = () => {
  return (
    <AnimatePresence exitBeforeEnter initial={false}>
      <Switch>
        <Route path="/page1">
          <Page1 />
        </Route>
        <Route path="/page2">
          <Page2 />
        </Route>
      </Switch>
    </AnimatePresence>
  );
};
Enter fullscreen mode Exit fullscreen mode

Transitions with AnimatePresence only work when the child immediately below the AnimatePresence component changes. In that case, that's our Switch component. We'll need to add some props to help AnimatePresence recognise when that change has occurred.

Once again, in App.tsx, we're going to import the useLocation hook from react-router.

import { useLocation } from 'react-router-dom';
Enter fullscreen mode Exit fullscreen mode

And now, let's add the location and key props to our Switch.

const App = () => {
  const location = useLocation();
  return (
    <AnimatePresence exitBeforeEnter initial={false}>
      <Switch location={location} key={location.pathname}>
        <Route path="/page1">
          <Page1 />
        </Route>
        <Route path="/page2">
          <Page2 />
        </Route>
      </Switch>
    </AnimatePresence>
  );
};
Enter fullscreen mode Exit fullscreen mode

Adding the transition effect parameters to our page components

Great news - we're done with our App component. Now, let's add some animations to our pages, and we should be ready to go!

Here's the Page1 component I'm working with. It's super simple - just a simple div with some styles, and a Link to our other page in the app.

const Page1 = () => {
  return (
    <div style={{ ...styles.page, ...styles.page1 }}>
      <p style={styles.copy}>This is page 1</p>
      <Link style={{ ...styles.copy, ...styles.link }} to="/page2">
        Go to Page 2
      </Link>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

To trigger an animation when this Page1 component is mounted, we'll need to wrap it in a special component called motion.div which is provided by Framer Motion. So, let's import that...

import { motion } from 'framer-motion';
Enter fullscreen mode Exit fullscreen mode

Now, we'll wrap our Page1 component in our new motion.div component, and provide some props to perform the animation when it's mounted.

const Page1 = () => {
  return (
    <motion.div
      initial={{ scaleY: 0 }}
      animate={{ scaleY: 1 }}
      exit={{ scaleY: 0 }}
      transition={{ duration: 0.5 }}
    >
      <div style={{ ...styles.page, ...styles.page1 }}>
        <p style={styles.copy}>This is page 1</p>
        <Link style={{ ...styles.copy, ...styles.link }} to="/page2">
          Go to Page 2
        </Link>
      </div>
    </motion.div>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, we've added three new props to our motion.div component here, too.

  • initial - This is the style of the component at the beginning of the animation when it is animating in.
  • animate - This is the style of the component at the end of the animation when it is animating in.
  • exit - This is the style of the component at the end of the animation when it is animating out.
  • transition - Transition configuration. Here, we're specifying how long we want the duration to last for (in our case, 0.5 seconds).

With those props in place, we can expect the following behaviour:

  • When the prop is first mounted, it is invisible (scaleY: 0)
  • Then, it will immediately animate over 0.5 seconds to be visible (scaleY: 1).
  • When it is animating out, it will resize back down before it is removed from the DOM (scaleY: 0).

Finally, the only other thing we need to do is wrap our other page components that we wish to animate using the same method.

I'm animating between two pages (Page1 and Page2 in this example), so I'll need to wrap Page2 in a motion.div tag, too.

const Page2 = () => {
  return (
    <motion.div
      initial={{ scaleY: 0 }}
      animate={{ scaleY: 1 }}
      exit={{ scaleY: 0 }}
      transition={{ duration: 0.5 }}
    >
      <div style={{ ...styles.page, ...styles.page2 }}>
        <p style={styles.copy}>This is page 2</p>
        <Link style={{ ...styles.copy, ...styles.link }} to="/page1">
          Go to Page 1
        </Link>
      </div>
    </motion.div>
  );
};
Enter fullscreen mode Exit fullscreen mode

And we're done!

And with that, we are done! We have successfully set up a fancy animation when navigating between pages in React.

You should now also have all the knowledge you need to customise your transition styles, too. Each page can also have different transition styles - the sky's the limit!

CodeSnap

CodeSnap Preview

If you enjoyed this course, I'm uploading tutorial videos, courses, articles and plenty more. If you'd like to see more of this content, please consider signing up for the mailing list over on CodeSnap.io. It encourages me to make more videos and articles just like this one 🙏

Thanks for reading!

Top comments (4)

Collapse
 
willem_rabe profile image
Willem Rabe

Cheers Sam,

Thanks for the write up!
How would you go about creating dynamic initial and exit animations?
I'm trying to have the page transition in or out differently depending on the link that is being clicked to load or leave the page.

Best, Willem

Collapse
 
sam_piggott profile image
Sam Piggott

Hey Willem!

Glad you enjoyed the guide :)

If I understand you correctly, to get your desired behaviour, it should be as straightforward as adjusting the initial, animate and exit props to fit your needs for each page separately. In the example I describe in the original post, we use the same props for both pages in order to gain the same transition when navigating through each page.

However, if you wanted to adjust, say, Page2's transition animation in, you simply need to change the properties for the Page2 component!

Here's an example. Below, Page1 would enter with a scaleY effect (stretching vertically), and Page2 would enter with a scaleX effect (stretching horizontally).

const Page1 = () => {
  return (
    <motion.div
      initial={{ scaleY: 0 }}
      animate={{ scaleY: 1 }}
      exit={{ scaleY: 0 }}
      transition={{ duration: 0.5 }}
    >
      <div style={{ ...styles.page, ...styles.page1 }}>
        <p style={styles.copy}>This is page 1</p>
        <Link style={{ ...styles.copy, ...styles.link }} to="/page2">
          Go to Page 2
        </Link>
      </div>
    </motion.div>
  );
};

const Page2 = () => {
  return (
    <motion.div
      initial={{ scaleX: 0 }}
      animate={{ scaleX: 1 }}
      exit={{ scaleX: 0 }}
      transition={{ duration: 0.5 }}
    >
      <div style={{ ...styles.page, ...styles.page2 }}>
        <p style={styles.copy}>This is page 2</p>
        <Link style={{ ...styles.copy, ...styles.link }} to="/page1">
          Go to Page 1
        </Link>
      </div>
    </motion.div>
  );
};
Enter fullscreen mode Exit fullscreen mode

You're not limited to just scaleX or scaleY - it can be any combination of any CSS properties! I'm just using these as a simple example.

Hope that helps!

Collapse
 
willem_rabe profile image
Willem Rabe

Hey Sam,

thanks so much for the thoughful reply!
I didn't quite make clear what I wanted to achieve:

Say I have pages A, B and C.
A link from page A to C should have a specific exit animation on A and a specific initial animation on C (say: A moves out to the left, C moves in from the right), while a link from B to C would have a different set (B exiting to the top, C moving in from the bottom). Think of a two-dimensional grid of pages you want to move around in.

Here's my somewhat clunky solution:

I first define a number of variants for initial and exit animations:

const animationVariants = {
  visible: { 
    x: 0,
    transition: { duration: 0.4, ease: "easeInOut" }
  },
  enterLeft:{
    x:'-100vw',
    transition: { duration: 0.4, ease: 'easeInOut' }
  },
  enterRight:{
    x:'100vw',
    transition: { duration: 0.4, ease: 'easeInOut' }
  },
  exitLeft:{
    x:'-100vw',
    transition: { duration: 0.4, ease: 'easeInOut' }
  },
  exitRight:{
    x:'100vw',
    transition: { duration: 0.4, ease: 'easeInOut' }
  }
};
Enter fullscreen mode Exit fullscreen mode

I then use framer motion's useCycle hook to keep track of what exit animation is currently set in state:

const [exitAnimation, cycleExitAnimation] = useCycle("exitLeft", "exitRight")
Enter fullscreen mode Exit fullscreen mode

I pass both the exitVariants and exitAnimation to my container element:

<motion.div 
  variants={animationVariants}
  initial={initAnimation}
  animate="visible"
  exit={exitAnimation}
    >
Enter fullscreen mode Exit fullscreen mode

In my react-router Link I have an anonymous function run onClick that will cycle the exit animation if it's not the one appropriate for this very link.

<Link onClick={
        () => {
          if(exitAnimation === "exitLeft"){
            cycleExitAnimation()
          }
        }
      }  to={{
          pathname: "/about/subpage",
          state: {prevPath: location.pathname}
          }}>A subpage</Link>
Enter fullscreen mode Exit fullscreen mode

You will notice I am also passing along a piece of state with the current pathname so the page loaded will correctly set the initAnimation in the container div.

const AboutSubpage = ( prevPath ) => {

  ...

  if(prevPath.location.state){
    if(prevPath.location.state.prevPath === '/about/another-subpage'){
      initString = "enterRight"
    }

  ...
Enter fullscreen mode Exit fullscreen mode

This all works, but it feels convoluted. Especially with more than two animations to cycle through. If I wanted to have four exit variants I'd end up with this odd construction in every single Link element:

      // order: "exitTop", "exitRight", "exitBottom", "exitLeft"
      // we want to cylce to exitBottom here...
      if(exitAnimation === "exitTop"){
        cycleExitAnimation()
        cycleExitAnimation()
      }else if(exitAnimation === "exitLeft"){
        cycleExitAnimation()
        cycleExitAnimation()
        cycleExitAnimation()
      }else if(exitAnimation === "exitRight"){
        cycleExitAnimation()
      }
Enter fullscreen mode Exit fullscreen mode

(useCycle doesn't accept parameters, hence the weird multiple calls)

it's just a lot of redundancy for something that feels like it should easily achievable. Maybe there is a way that's more maintainable?

Cheers,
Willem

Collapse
 
jacksonkasi profile image
Jackson Kasi

hey, please give me a full code. i am new for framer