Page transitions are cool and can add that extra layer of polish to your web app.
In this post, I'll explain how to add a very basic page transition to your Next.js app. We are not going to do anything super fancy. The page transition that we're going to use is based on spring animations that generally produces more believable animations since they behave similarly to objects in the physical world.
Next.js setup
I am going to assume you already have a Next.js app. If not, go ahead and set one up now.
The easiest is to run the following command from your terminal:
yarn create next-app
You are going to need a custom Next.js App Component. If you don't have one already, inside you pages
directory add a new file called _app.tsx
(or _app.js
if you're using JavaScript).
Add the following React component as a starting point:
import { AppProps } from 'next/app'
function CustomApp({ Component, pageProps }: AppProps): JSX.Element {
return <Component {...pageProps} />
}
export default CustomApp
This custom App does nothing more than what Next.js does out of the box. For each page in your app, it will be called with Component
and pageProps
props, which is the specific page component and the initial props object passed to it via one of Next.js' page data fetching methods, like getStaticProps
or getServerSideProps
.
The Next.js Custom App component is the perfect place to add any components and other code that will be used for every page of your app. Things like
ThemeProvider
s and global style resets. We will leave those out to keep this example focused on the page transition.
react-flip-toolkit
We will make use of the react-flip-toolkit
library to apply the spring-based animation between our pages. This library is based on Paul Lewis's FLIP animations idea. It is worth reading Paul's post if you haven't already. The FLIP concept can be applied to so many different animation problems.
We are mostly interested in the Flipper
and Flipped
components that the library exports for us. Let's take a look at those.
Flipper
The Flipper
component wraps around all the child components that will form part of the animation. You can think of it as a React Context Provider. This component takes one mandatory prop, flipKey
, which it uses to determine when the children should be animated. When the key changes, an animation triggers.
Because we want to trigger our transition animation every time the route changes, it naturally makes sense that we use the page's route as the flipKey
. Lucky for us, the Next.js custom app component also receives a router
prop. Let's add this to our CustomApp
:
import { AppProps } from 'next/app'
import { Flipper, Flipped } from 'react-flip-toolkit'
function CustomApp({
Component,
pageProps,
router, // Added this prop
}: AppProps): JSX.Element {
return (
// Wrapping our page `Component` with the `Flipper`
<Flipper flipKey={router.asPath}>
<Component {...pageProps} />
</Flipper>
)
}
export default CustomApp
You might have noticed that we used the asPath
attribute of the router
object and not pathname
. That is because if we are using dynamic routes, the pathname
will be the same for pages matching the same dynamic route. The asPath
, on the other hand, is the actual path, including the query params.
Flipped
At this stage, our page transition is still not working, and that is because we haven't told the Flipper
which child components should be animated. That is where the Flipped
component comes in.
The Flipped
component wraps each animatable component inside the Flipper
parent component. To identify each individual animatable component, a flipId
prop is required for the Flipped
component. Each component that has the same flipId
will be animated from its previous state to the new state when the Flipper
is updated. This is very useful if you want to animate a list of items that get reordered.
The
Flipped
component does not necessarily have to be a direct child ofFlipper
. It can be deeply nested.
In our case, we don't have a list of items to animate; we want to animate the content of one page into the next. We can, therefore, use the same flipId
for all our pages. Let's be extremely creative and call this flipId
page
:
import { AppProps } from 'next/app'
import { Flipper, Flipped } from 'react-flip-toolkit'
function CustomApp({ Component, pageProps, router }: AppProps): JSX.Element {
return (
<Flipper flipKey={router.asPath}>
// Wrapping our page `Component` with a `Flipped` component
<Flipped flipId="page">
<Component {...pageProps} />
</Flipped>
</Flipper>
)
}
export default CustomApp
Depending on how your app's page components use additional props, the page transition might or might not work already. The Flipped
component will pass a couple of flippedProps
down to its direct child component, i.e. the <Component {...pageProps} />
component. These props are used to set the necessary opacity, translate and scale values on the underlying DOM elements. For this reason, these flippedProps
need to be spread into a native DOM component ultimately, like a <div>
or <span>
and not into a custom React component, like <CustomComponent>
.
The easiest to do this, is just to wrap the <Component {...pageProps} />
component in our CustomApp
with another <div>
:
import { AppProps } from 'next/app'
import { Flipper, Flipped } from 'react-flip-toolkit'
function CustomApp({ Component, pageProps, router }: AppProps): JSX.Element {
return (
<Flipper flipKey={router.asPath}>
<Flipped flipId="page">
// Wrapping our page `Component` with a native `div`
<div>
<Component {...pageProps} />
</div>
</Flipped>
</Flipper>
)
}
export default CustomApp
With this change, the Flipped
component is always able to apply the necessary animation props to the underlying div
. The only outstanding part that might stop your page transition from working is if your app is not using client-side routing.
I am not going to say page transitions are impossible if you're using server-side routing, i.e. page refreshes, because people always do amazing things with the web that I thought was impossible. I will say that it would probably not be worth the effort.
Luckily for us, Next.js makes client-side routing very easy and will default to that if we use their Link
component correctly and our pages do not use getServerSideProps
as the data fetching method. As the name suggests, that will always happen server-side.
Client-side Routing
The recommended declarative way to route within your Next.js app is to use the <Link>
component available through the next/link
package: import Link from 'next/link'
A link to your "About" page could look like this:
<Link href="/about">
<a>About</a>
</Link>
This will work fine if you have an about.tsx
or about.js
file in your pages
directory and that component doesn't rely on server-side data fetching for each page render.
If you have dynamic page routes, it is slightly different. Let's say you have the following pages:
pages /
articles /
[article].tsx
If you want to create a Link
to an individual article page, it would have to look something like this:
<Link href="/articles/[article]" as="/articles/the-article-slug">
<a>Read more</a>
</Link>
If you forget to add the as
and href
prop, the page will always perform a refresh and load server-side. Make sure to use this correctly throughout your app.
Sometimes you want to route programmatically within a callback or event handler. For that, you can use the Next.js router
directly. If you're using a React function component, the easiest is to use the useRouter
custom hook: import { useRouter } from 'next/router'
.
For our previous page examples, you would be able to route like this:
router.push('/about')
Or for the dynamic article route:
router.push('/articles/[article]', '/articles/the-article-slug', {
shallow: true,
})
Note, the shallow: true
option. That is necessary to force client-side-only routing.
With these things in place, your page transitions should be working. If for some reason it isn't, check here for ideas to troubleshoot.
Check out this example repo if you want to see it in action.