DEV Community

Ivan V.
Ivan V.

Posted on

Full Page Transitions with Next.js

I'm going to show you how to do full page transitions with Framer Motion and Next.js
Basic familiarity with Framer Motion and next.js is good to have, but the code is short and not complicated so everyone should be able to follow along.

Check out the demo: https://nextjs-full-page-transitions.netlify.app/
Fork the repository: https://github.com/ivandotv/nextjs-page-transitions

What we are building

slide-right
fade-back
rotate-z

Not only we are going to enable full page transitions but we are also going to set up a few different transitions for demo purposes.

The code

Spoiler alert, this is all the code that is needed to enable page transitions in Next.js!

// _app.tsx
function MyApp({ Component, pageProps, router }: AppProps) {
  return (
    <div className="app-wrap">
      <LazyMotion features={domAnimation}>
        <AnimatePresence exitBeforeEnter>
          <m.div
            key={router.route.concat(animation.name)}
            className="page-wrap"
            initial="initial"
            animate="animate"
            exit="exit"
            variants={animation.variants}
            transition={animation.transition}
          >
            <Component {...pageProps} />
          </m.div>
        </AnimatePresence>
      </LazyMotion>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now let us go through it step by step.

First, you will notice that I'm using LazyMotion component, instead of regular Motion component, this is just for reducing the size of the bundle. Framer is not a small library
(around 25kb full), and since this code goes into the Next.js _app component, which is the main component in the framework, everything that is imported there, will be bundled in the initial bundle download

When you build the project this is indicated by the First Load JS shared by all item in the Next.js build report.

Granted, LazyMotion will not slice the size of the bundle by much (around 5-7kb) but why not use it when it's available. Also, the folks that support Framer
are working on reducing the bundle size even more, so you are ready to receive more size savings in the future.

You will notice that we are using features={domAnimation} on the LazyMotion component which means that we are only bundling a subset of Framer functionality. Things like pan and drag gestures and layout animations will not be available. Check out the official documention for the component to know more.

AnimatePresence Component

AnimatePresence component is used for animating child components when they are removed from the React tree. It
allows the component to defer unmounting until after the animation is complete. The most crucial property for this component is
exitBeforeEnter, it allows Framer to animate one component at a time.
So the page, which is the Component in this whole setup, after a route change will animate out and then the new page (which is also a Component) will animate in. So there will be no overlap and we only get to see one page at any given time.

UPDATE:
I have updated the demo to show how transitions work when exitBeforeEnter is set to false. When this property is false it will enable animations on both pages (new and old at the same time). Make sure to enable the "overlap page transitions" checkbox.

rotate-z

rotate-z-overlap

m.div component

Motion and m components are the main building blocks for Framer animations. Anything you want animated should go inside these components.

m.div component is very similar to regular Framer Motion component. It is an optimized version that works in tandem with LazyMotion.

By default, the motion component comes pre-bundled with all of its features. The m component can be used in the same way as Motion, but it comes with no features preloaded. These are then provided by LazyMotion.

Animations

Framer supports a lot of different ways to create the actual animations, in this demo we are going to use the labels functionality of Framer to animate the components. Take a look at this basic example, it will animate ComponentToAnimate opacity from 0 to 1 and then back to 0.

function MyApp() {
  return (
    <m.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.7 }}
    >
      <ComponentToAnimate />
    </m.div>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • initial - How the element should look when the element is mounted into the React tree (before the animation starts)
  • animate - How it should look when the animation ends (basically when it's animated into position)
  • exit - How it should look when it's animated out (just before it's removed from the React tree )
  • transition - how will the actual animation behave duration, easing, etc. It is similar to the css transitions property.

Our page animations are a little bit more complicated than that, and we are also dynamically changing animations in the demo, so we are going to add another property to the m.div component

  • variants - this will allow us to organize animations as objects, and refer to them by name, and switch between them on demand.

Simple example is worth 1000 words:

const myAnimation = {
  initial: {
    opacity: 0
  },
  animate: {
    opacity: 1
  },
  exit: {
    opacity: 0
  },
  transition: {
    duration: 0.7
  }
}

function Component() {
  return (
    <m.div
      initial="initial"
      animate="animate"
      exit="exit"
      transition={myAnimation.transition}
      variants={myAnimation}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

So now we can easily switch animations by providing a different object to the variants property (myAnimation). In the demo, we are doing this via the HTML dropdown elements and simple useState hook. You can refer to the animations.ts file to see all the animations that are used in the demo

// animations.ts excerp
const slideUp = {
  name: 'Slide Up',
  variants: {
    initial: {
      opacity: 0,
      top: '100vh',
      scale: 0.4
    },
    animate: {
      opacity: 1,
      top: '0vh',
      scale: 1
    },
    exit: {
      opacity: 0,
      top: '100vh',
      scale: 0.4
    }
  },
  transition: {
    duration: 0.7
  }
}

const slideRight = {
  name: 'Slide Right',
  variants: {
    initial: {
      opacity: 0,
      left: '-100%',
      scale: 0.6
    },
    animate: {
      opacity: 1,
      left: 0,
      scale: 1
    },
    exit: {
      opacity: 0,
      left: '100%',
      scale: 0.6
    }
  },
  transition: {
    duration: 0.7
  }
}
Enter fullscreen mode Exit fullscreen mode

And that's it. As you can see, full-page transitions in Next.js with Framer Motion library are not that complicated :)

Check out the demo: https://nextjs-full-page-transitions.netlify.app/
Fork the repository: https://github.com/ivandotv/nextjs-page-transitions

Oldest comments (5)

Collapse
 
raymolla7 profile image
raymolla7

Hi Ivan, this is wonderful but I am having issue.

I followed this and I have the enter animations working. However I can't seem to get the Exit animations working.

The only thing I did different from you is that I didn't implement framer motion in my _app.tsx but rather in a sub page component. Could this be the reason why?

Collapse
 
ivandotv profile image
Ivan V.

Probably. My code only animates top level components (pages).

Collapse
 
deansacramone profile image
Deano Macaroni

@raymolla7 Did you get this working? I ask because I have the same need. I do NOT want to animate the top level as my animations are for a feature within the app, not the whole app... like only a section of the site. So, I only want to animate only a small section of the site.. So, did you get this working? If so, do you have a repo I can look at? Thanks a bunch.

Collapse
 
cjsmocjsmo profile image
Charlie J Smotherman

Thanks for this I found it useful however when trying to build and run the repo I get this:

next-animation@0.1.0 dev
next dev

ready - started server on 0.0.0.0:3000, url: localhost:3000
info - Using webpack 5. Reason: Enabled by default nextjs.org/docs/messages/webpack5
Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:71:19)
at Object.createHash (node:crypto:133:10)
at BulkUpdateDecorator.hashFactory (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:138971:18)
at BulkUpdateDecorator.update (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:138872:50)
at OriginalSource.updateHash (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack-sources3/index.js:1:10264)
at NormalModule._initBuildHash (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68468:17)
at handleParseResult (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68534:10)
at /home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68628:4
at processResult (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68343:11)
at /home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68407:5
Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:71:19)
at Object.createHash (node:crypto:133:10)
at BulkUpdateDecorator.hashFactory (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:138971:18)
at BulkUpdateDecorator.update (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:138872:50)
at OriginalSource.updateHash (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack-sources3/index.js:1:10264)
at NormalModule._initBuildHash (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68468:17)
at handleParseResult (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68534:10)
at /home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68628:4
at processResult (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68343:11)
at /home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68407:5
node:internal/crypto/hash:71
this[kHandle] = new _Hash(algorithm, xofLen);
^

Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:71:19)
at Object.createHash (node:crypto:133:10)
at BulkUpdateDecorator.hashFactory (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:138971:18)
at BulkUpdateDecorator.update (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:138872:50)
at OriginalSource.updateHash (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack-sources3/index.js:1:10264)
at NormalModule._initBuildHash (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68468:17)
at handleParseResult (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68534:10)
at /home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68628:4
at processResult (/home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68343:11)
at /home/charliepi/nextjs-page-transitions/node_modules/next/dist/compiled/webpack/bundle5.js:68407:5 {
opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
library: 'digital envelope routines',
reason: 'unsupported',
code: 'ERR_OSSL_EVP_UNSUPPORTED'
}

Node.js v18.12.1

Collapse
 
kevinhekh profile image
kevinhe-kh

same problem i have.....don't know what's the cause.