Framer Motion is an incredibly well-made animation library that makes it super-easy to add smooth animations to your React-based site.
Pair Framer Motion with Next.js and you can add stunning page transitions that aren't possible with a traditional server-first website!
In this article, I will show you how to add page transitions to Next.js and some hidden gotchas to watch out for.
After setting up a basic Next.js app and installing framer-motion, follow these steps:
pages/_app.tsx
import { AnimatePresence } from 'framer-motion'import { AppProps, useRouter } from 'next/app'export default function App({ Component, pageProps }: AppProps) {const router = useRouter()const pageKey = router.asPathreturn (<AnimatePresence initial={false} mode="popLayout"><Component key={pageKey} {...pageProps} /></AnimatePresence>)}
It's a simple piece of code but there are some important things going on:
If you leave this out, framer-motion will animate the page on first load, which will cause visual jitter in a server-rendered app.
This does two things:
position: absolute;
while it's animating out to make room for the next page<Component>
is the component of the page (from the pages/
directory) that is currently being displayed.
We're using the complete path of the current route to give it a unique key
property.
Without this key
property, framer-motion can't distinguish two separate pages that use the same component, for example if
/post-1
and /post-2
were both rendered by pages/[slug].tsx
.
The <PageTransition>
component will define our in- and out-animations.
The following component slides pages in from the right and slides them out to the left:
components/PageTransition.tsx
import React, { forwardRef, useMemo } from 'react'import { motion, HTMLMotionProps } from 'framer-motion'type PageTransitionProps = HTMLMotionProps<'div'>type PageTransitionRef = React.ForwardedRef<HTMLDivElement>function PageTransition({ children, ...rest }: PageTransitionProps,ref: PageTransitionRef) {const onTheRight = { x: '100%' }const inTheCenter = { x: 0 }const onTheLeft = { x: '-100%' }const transition = { duration: 0.6, ease: 'easeInOut' }return (<motion.divref={ref}initial={onTheRight}animate={inTheCenter}exit={onTheLeft}transition={transition}{...rest}>{children}</motion.div>)}export default forwardRef(PageTransition)
One important thing is that we're forwarding the ref to <motion.div>
. This is required because we're using <AnimatePresence mode="popLayout">
.
This is the final puzzle piece. We include the <PageTransition>
component at the top
of each page that we would like to equip with our fancy animated page transitions.
pages/index.tsx
type IndexPageProps = {}type IndexPageRef = React.ForwardedRef<HTMLDivElement>function IndexPage(props: IndexPageProps, ref: IndexPageRef) {return (<PageTransition ref={ref}><div className="IndexPage">{/* ... */}</div></PageTransition>)}export default forwardRef(IndexPage)
Note that, again, we're forwarding the ref to the <PageTransition>
component, completing a chain of ref-passing from <AnimatePresence>
all the way down to the top-level <motion.div>
.
AnimatePresence -> PageComponent -> PageTransition -> motion.div
Is this really how easy it is to add page transitions to a Next.js app?
Kind of! But there are some gotchas:
If your pages are long enough so that the user has to scroll, you will notice that the page gets scrolled to the top before the page transition happens.
One easy (slightly hacky) way to prevent this, is to give your PageTransition
component max-height: 100%; overflow-y: auto;
.
This way, each page basically gets its own scroll container.
It's hacky, because usually the Next.js handles scrolling of the page and e.g. sets the scroll position correctly when you use the back-button in your browser.
Solution 2 doesn't do that any better is slightly more complex, but it's good to know your options:
If we wait for Page A to exit before we mount Page B and animate it in, we don't have to deal with two pages with separate scrolling positions at the same time.
We can then tell Next.js not to scroll to top automatically when the route has transitioned (this is when the page transition starts), but instead handle scrolling ourselves as soon as Page A has left the viewport.
So this solution has two pieces to it:
Change the mode
prop to "wait"
and scroll to the top of the page when a component exits (e.g. Page A gets unmounted).
const onExitComplete = () => {window.scrollTo({ top: 0 })}// ...return (<AnimatePresenceonExitComplete={onExitComplete}mode="wait"initial={false}>{/* ... */}</AnimatePresence>)
There isn't currently no way to disable Next.js' scroll restoration globally, so make sure to edit every <Link>
component to have scroll={false}
(see docs)
and any imperative route transitions using something like router.push({ scroll: false })
(see docs).
Notice how we have a blank screen in-between exit and enter transitions.
This is because we now wait for Page A to exit completely before animating Page B in.
I've solved the scroll jumping problem in the past by giving the exiting page position: fixed;
and a calculated offset
to account for the current scrolling position.
It worked pretty well but was pretty complicated to implement and I would rather use Solution 1 these days, simply giving each page its own scroll container.
The more you think about page transitions, the more you see how little the web is prepared for it at the moment, although there are some promising things in the works with the View Transitions API.
Something else to think about, is how animations should behave when hitting the back-button.
This is likely dependent on your animation but actually finding a way to disable animations while using the back-button would be nice!
Another interesting thing happens when building page transitions with Next.js.
To illustrate it, let me make a simple page that shows the current path:
function PathPage(props: PathPageProps, ref: PathPageRef) {const router = useRouter()return (<PageTransition style={style[num]} ref={ref}>{router.asPath}</PageTransition>)}
See what happens when the pages transition:
It's a little subtle but the text of {router.asPath}
updates for the page doing the exit-transition.
This can cause very subtle bugs and it's important to be aware of this.
A workaround can be to remember the router's state when the page first mounted:
// Storing asPath in a state variable will make sure it doesn't change// after initialization:const asPath = useState(router.asPath)
Page transitions are fancy and can really wow your customers. It's really easy to add them using framer-motion and Next.js.
There are some problems and trade-offs to be aware of, so use page transitions wisely and keep an eye out for a proper solution like the View Transitions API.
Hi, I’m Max! I'm a fullstack JavaScript developer living in Berlin.
When I’m not working on one of my personal projects, writing blog posts or making YouTube videos, I help my clients bring their ideas to life as a freelance web developer.
If you need help on a project, please reach out and let's work together.
To stay updated with new blog posts, follow me on Twitter or subscribe to my RSS feed.