Written by Francois Brill✏️
The web has become increasingly more interactive. Users have come to expect a higher level of interactivity to grab their attention and induce them to engage with the information on the page. A key way to capture attention is to use movement and page transitions. This may sound like a PowerPoint slide deck with inbuilt clipart-type page transitions, but I can assure you that’s not what I’m referring to!
This article will demonstrate how to use Next.js and Framer Motion to apply subtle, elegant page transitions that add personality and style to your site.
Jump ahead:
- Indicating loading state
- Determining when to use page transitions
- Considering user experience
- Building page transitions
- Page transitions demo
- Bonus tip: Loading page transitions
Indicating a loading state
Page transitions are just one of several tools in a frontend developer’s toolbox. When used appropriately, page transitions can increase user engagement, even holding a user’s attention during page load.
Webpage content generally content loads quickly, but if you’re dependent on fetching data from a server, a page transition with just the right level of motion applied could help keep the user engaged.
One option is to add a quick loading bar; this provides feedback to a user and informs them to stay put and not navigate away when content takes an extra couple of seconds to load.
Anything faster than 200ms is perceived by the brain to be instant. However, it’s really hard to get a page to load that fast. By adding a .5-2s page transition, you’re essentially buying yourself time to load data and get things ready, so that when the next page is revealed you can just deliver the content that the user requested.
Determining when to use page transitions
In order to provide the best website UX, you’ll need to put the user at the center of the experience and consider things from their perspective. Consider the context of the user’s needs when visiting your website and use this information to evaluate what level of page transitions are acceptable.
The loading bar mentioned previously could be used on nearly any type of website. The architecture of the loading bar could range from a full-page experience to a slim component that is only visible at the top of the webpage. But, regardless of the loading bar’s appearance, its purpose is to indicate the system status as the next page is loading.
When users visit an information-rich website to consume content, they do not want to be slowed down by page transitions. This is especially true of sites where users browse several pages. So, don’t get too excited and add page transitions to your corporate website! More flamboyant page transitions should be reserved for websites that are more creative in nature since their site visitors likely expect more entertainment.
I’ve added some page transitions, am I done?
In general, animations are most successful when they are subtle, feel natural, and mesh well with the entire overall user experience. Simply adding page transitions would probably feel out of place, and you’d need to consider the overall on-page experience. You might want to add some movement to elements to enter the page, interactive hover effects, and so forth to keep the user engaged throughout their visit to your website.
Building page transitions
Page transitions can be built in just about any frontend framework or technology, but in this demonstration, we’ll use Next.js to provide the cue for when pages transition to trigger the animation and Framer Motion to actually perform the page transitions.
Framer Motion has dubbed itself “the production-ready motion library for React”, and it’s a real treat to use. The thing I like most about this library is its declarative way of achieving animations. With Framer Motion, you declare what you want the start and end to look like, and the library fills in the gaps.
Creating animations from scratch is really difficult. If you’ve ever attempted to use CSS, or almost any other language, to animate something in React, it becomes very tricky. React immediately unmounts the element that is exiting the DOM and, because the element is dropped, you can’t animate it on the way “out”. A page transition would not feel right if there was an abrupt jump when the page changed. This is part of the magic that Framer Motion automatically takes care of for us, although there is a trick to implementing it that I’ll cover below.
OK, let’s jump in and put this all to use!
Page transitions demo
To demonstrate creating page transitions, we’ll build a Next.js site with Framer Motion. We’ll style the site with my preferred method: Tailwind CSS.
Here’s what we’ll end up with; each photo page is a new (dynamic) page in Next.js and you can see the page transitions as we navigate between the list and detail pages:
If you’d like, you can also grab the source code for the above example to browse and follow along.
Setting the scene
To set up our page transitions demo, we need to understand how the internal workings of Next.js function:
- First, there is an
_app.js
file that is persisted between page loads - Second, we need to use the Next.js
<Link>
component to link to pages. With this, Next.js performs a shallow render and essentially mounts the new page component while it unmounts the previous component, giving us an SPA-like feel and the ability to change between pages without having to reload the page. We need this functionality in order to achieve page transitions - As mentioned previously, the most difficult part of trying to animate React components that leave the DOM is that they are simply just gone, making them nearly impossible to animate. Framer Motion solves this with an
<AnimatePresence>
component that does some magic to make it possible to declare anexit
state that can be animated
Starting a new Next.js site
To showcase how we can achieve animated page transitions, let’s create a quick Next.js site with the Tailwind CSS starter to handle our styling.
Next, we’ll need to install Framer Motion, like so:
yarn add framer-motion
Adding AnimatePresence
Now, we’ll work on setting up the page transitions. First, we add the AnimatePresence to _app.js
:
// _app.js
import { AnimatePresence } from 'framer-motion'
function MyApp({ Component, pageProps, router }) {
return (
<AnimatePresence mode="wait" initial={false}>
<Component {...pageProps} key={router.asPath} />
</AnimatePresence>
);
}
Next, we need to wrap our <Component>
with <AnimatePresence>
.
Here are two additional, but optional, settings that I enabled for this demo:
-
mode="wait"
: This just tells Framer Motion to complete any exit animations (exiting page) before starting a new animation (new page) on the new component. -
initial
: Setting this tofalse
means it’s not going to play the animation on the first page load, which just feels better.
One key point here is to make sure your animated elements are direct children of AnimatePresence
so that it can take over and animate any exit events before removing the element from the React tree.
Because we’re declaring AnimatePresence
in the_app.js
and AnimatePresence
animates the direct children, we need to provide the <Component>
that we’re returning a unique key to. Initially, this tripped me up. I resolved this issue by adding the page path as a key to ensure it’s always unique. This way, React will register each page as a different component and can animate the exit before animating the entry of a new component.
Creating a shared layout component
Once the wrapper is in place in the _app.js
, we’ll need to create the child page element that is actually animated. Instead of doing this on each and every page, we can create a shared <Layout>
component that can be used to wrap all the pages we want to animate:
// components/Layout/index.js
import { motion } from "framer-motion";
const Layout = ({ children }) => (
<motion.div
initial={{ x: 300, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 300, opacity: 0 }}
transition={{
type: "spring",
stiffness: 260,
damping: 20,
}}
>
{children}
</motion.div>
);
export default Layout;
These are some great default settings to start off with, but you can explore other declarative properties too. For example, you could use initial
to specify a transition starting point, where the element should “come from”, while animate
would specify the “end state” of where you want things to end up, exit
is used to specify the target of where the animated component should end up.
These properties can be used to fine-tune any page transitions, as well as the transition
properties itself. If these names sound confusing, you can also define your own variants
to make it easier to follow.
Using the layout component
Next, we need to use the layout component on all the pages we want to animate; these could be static pages or dynamic routes - it really does not matter.
Make this is the first component wrapping any child component to ensure it’s a direct descendant of <AnimatePresence>
:
// pages/index.js
import Layout from "../components/Layout";
export default function Home() {
return (
<Layout>
// ....
// Page content goes here
// ....
</Layout>
);
}
With the above stripped-down markup of the homepage, I’m just showing you what is required to get the page transitions working. Performing this on multiple pages and linking to them will result in a page transition, and any content in the actual component will animate with our page transition.
Scrolling back to top
With my example, it was hard to see if the transitions were working at first as the pages weren’t that long. But in looking at the mobile experience, it is clear what is happening. Once we scroll down and transition to the new page, Next.js persists the position and we land in the middle of the new page. This is clearly not a great user experience:
Fixing this issue is easy with the attribute on our root <AnimatePresence>
where we can add any onExitComplete
function. All we have to do is scroll the window back to the top once the exit animation is complete and, regardless of the page length, the new page will start from the top:
// _app.js
<AnimatePresence
mode="wait"
initial={false}
onExitComplete={() => window.scrollTo(0, 0)}
>
Bonus tip: Loading page transitions
As I alluded to in the beginning of this guide, page transitions that also serve as a page loader are helpful for keeping a user’s attention while you’re fetching data.
Next.js makes this easy by providing us with some Router
events that we can use to create this:
// _app.js
import { useState, useEffect } from "react"
import Router from "next/router"
import PageLoader from "../components/PageLoader"
const App = ({ Component, pageProps }) => {
const [loading, setLoading] = useState(false)
useEffect(() => {
// Used for page transition
const start = () => {
setLoading(true)
}
const end = () => {
setLoading(false)
}
Router.events.on("routeChangeStart", start)
Router.events.on("routeChangeComplete", end)
Router.events.on("routeChangeError", end)
return () => {
Router.events.off("routeChangeStart", start)
Router.events.off("routeChangeComplete", end)
Router.events.off("routeChangeError", end)
}
}, [])
return loading ? <PageLoader /> : <Component {...pageProps} />
}
export default App
We use the Next.js Router
events to set a local state variable to indicate the loading state. From there we can decide what to do with that indicator. In this example, there is a different component that will be rendered as a whole page loader, which could be styled and animated separately.
Conclusion
In this article, we looked at when and why you may want to consider adding page transitions on your site. We demonstrated how to create and add page transitions using Next.js and Framer Motion. We also looked at a different approach for using a page loader as an interstitial loading state that serves as a page transition. If you try out this approach, use the comments below to let me know how you find it!
LogRocket: Full visibility into production Next.js apps
Debugging Next 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 Next 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 Next.js apps — start monitoring for free.
Top comments (0)