While redesigning my website I decided to add page transition animations to improve the user experience. In turn, this led me down the rabbit hole of web-based animations, of which I came back with the idea of using Framer Motion.
Framer Motion, for the uninitiated, is a JavaScript animation library that has many powerful features. In fact, it has so many you could make an entire course on it. But, that's not what we're covering today.
Today, we are going to be looking at using the AnimatePresence
component. And, more specifically how it can help us with page transition animations in GatsbyJS.
Before we jump in, let's take a look at what we'll be covering:
Let's get started.
AnimatePresence
React itself has no lifecycle method for:
- Notifying components when they're going to be unmounted.
- Allows for the defering of the unmounting until an operation is complete.
This is what AnimatePresence
aims to solve.
By wrapping all our components in an AnimatePresence
component, we can provide exit animations to components that will be completed once a component is going to be unmounted.
In other words, this means that if we remove a component from the DOM tree, it will fire its exit animation, wait for it to complete and then be removed. This is unlike normal React where it would be removed without waiting for the animation to complete.
Read more about AnimatePresence here.
exitBeforeEnter
While AnimatePresence
provides the capabilities for us to use exit animations. If we want to ensure the exit animation of the leaving component is complete before the entering component starts animating, we need to add a prop.
This prop is exitBeforeEnter
, read more here.
exitBeforeEnter
forces only one component to render at a given moment. This means that the exit animation of the leaving component will complete before the entering component starts animating.
With the theory covered, let's jump into some code.
Page Transition Animations with GatsbyJS
As mentioned earlier to use AnimatePresence
we need to wrap all our components in it. For page transitions, this means that we need to wrap each page in it.
This is easy to do in GatsbyJS.
With GatsbyJS, when you create a new file in the pages directory it will automatically become a new route on the website to navigate to.
For example, create a file called blog.js
it will be available at the URL yourdomain.com/blog
, pretty nifty.
A good method of ensuring each page looks the same and shares the same common elements is to wrap each page in a Layout
component.
This way we can add our common components and styles to one component and they'll apply to each page automatically.
This also provides us with a great place to add our AnimatePresence
component.
If we went into our Layout
component and added AnimatePresence
like so:
export default function Layout({ children, path }) {
return (
<>
<Nav path={path} />
<AnimatePresence exitBeforeEnter>
<main>{children}</main>
</AnimatePresence>
<Footer />
</>
);
}
You would think this would now allow us to use exit animations on our pages but we can't... Why?!?
The reason this doesn't work is that although every page shares the Layout
component, it doesn't stay mounted all the time.
Because of how GatsbyJS works it actually unmounts and remounts the Layout
component every time you change a page. In effect, this means that each time you change the page you're getting a new AnimatePresence
component that has no memory of the previous components.
For AnimatePresence
to be able to monitor each component as they mount and unmount, we need to keep it mounted at all times.
Don't worry we have a solution for this.
wrapPageElement
Gatsby has exposed several APIs in the gatsby-browser.js
file for us to use. One of these is wrapPageElement
, which solves our problem perfectly.
The wrapPageElement
API allows us to wrap every page in a component like the Layout
component from before. But, this time wrapPageElement
prevents the component from being unmounted on page changes.
So we can add our AnimatePresence
component here and because it won't unmount like before, it can monitor each mount and unmount.
Here's what it looks like:
export function wrapPageElement({ element, props }) {
return <AnimatePresence exitBeforeEnter>{element}</AnimatePresence>;
}
With this in place, we are now free to add entry and exit animations to our pages, let's do that now.
Read more on Gatsby Browser APIs here.
Adding our Animations
Because we now have AnimatePresence
wrapping each of our pages, the only thing we have to do is provide the animation.
Going back to our Layout
component from earlier, we can substitute out our original main
element for a motion.main
element. This allows us to apply animations to the element.
For our page transition animation to work smoothly, we need to add 5 props to our new motion.main
element, these are:
- key
- initial
- animate
- exit
- transition
Let's cover each of these now.
Key
Like normal React when you create components by mapping over data you need to add a key
prop to them. This is for React to keep track of each of the components and aid in unmounting them as required.
The same goes for Framer Motion. So it knows when to fire an exit or entry animation, it needs to be able to keep track of each animated component. This is done by providing a unique key
prop to each element.
Initial
The initial
prop is the state the component will start in when it is first mounted.
In our case, I want the component to fade in when it's entering. So we set the initial
prop to initial={{ opacity: 0 }}
.
Animate
The animate
prop is the state of the component that it will animate to from the state defined in the initial
prop.
So for us to complete the fade-in animation we want, we set the animate
prop to be: animate={{ opacity: 1 }}
.
This means that when the component first mounts, it starts at opacity: 0
and then works up to opacity: 1
.
Exit
When the component unmounts it needs a state to animate to from the state we defined in the animate
prop. This is what we define in the exit
prop.
For the exit animation, I want the component to fade out again. So it starts at opacity: 1
as defined by the animate
prop and it will finish at opacity: 0
, defined in the exit
prop like exit={{ opacity: 0 }}
Transition
Finally, we have the transition
prop. This prop is the settings of the entire animation. Here, we can define the type of animation, the duration and a bunch of other things.
I won't cover all the settings here but the settings I used for my animation was:
transition={{
type: 'spring',
mass: 0.35,
stiffness: 75,
duration: 0.3,
}}
If you're interested you can read about the transition
prop here.
Finished Component
That's all the props covered for our animation, let's take a look at the finished component:
export default function Layout({ children, path }) {
return (
<>
<Nav path={path} />
<motion.main
key={path}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: "spring",
mass: 0.35,
stiffness: 75,
duration: 0.3,
}}
>
{children}
</motion.main>
<Footer />
</>
);
}
We these props now added in, our page transition animations should now be working.
Summing Up
As with many things; once you know-how; it's not too difficult. Even so, adding page transition animations with Framer Motion was a lot less challenging than I expected it to be.
If for some reason these steps did not work for you, please feel free to DM me on Twitter and I'll be happy to help.
If you found this post helpful, I would greatly appreciate it if you could share this post with others who you think may find it useful.
If you want to support me and see more of my content, please consider following me on Twitter where I post more daily web development content.
Until next time, thanks for reading. ๐