motion: Can't get shared layout animations to work with Next.js 13

1. Read the FAQs šŸ‘‡

2. Describe the bug

With Next 13, using separation between server and client components, shared layout animations don’t work when implementing the seemingly trivial navigation menu underline. Using either the deprecated or the modern approach (ie. layoutId on the target components).

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

This issue could be heavily dependent upon styles and nesting(s). The structure honors HTML and React semantics, but it runs complex animations. Hence: the repro is complete, interactive, commentable and with full source code.

Live: https://portfolio-2023-git-feature-implement-layout-maurocolella.vercel.app/

Implementation: https://github.com/maurocolella/portfolio_2023/pull/2/files#diff-f7c999b982bfdbf5f3790e5b148e2343ffb7611b9ba579756dabab9fc76cb2e7

4. Steps to reproduce

On the example page: https://portfolio-2023-git-feature-implement-layout-maurocolella.vercel.app/

Simply click navigation items. Transition doesn’t take place.

5. Expected behavior

I would expect a smooth transition to occur as in the examples.

6. Video or screenshots

N/A.

7. Environment details

Ubuntu Linux 20.04, Chrome beta, Chrome stable, Firefox.

FAQs

Framer Motion won’t install

Framer Motion 7+ uses React 18 as a minimum. If you can’t upgrade React, install the latest version of Framer Motion 6.

height: "auto" is jumping

Animating to/from auto requires measuring the DOM. There’s no perfect way to do this and if you have also applied padding to the same element, these measurements might be wrong.

The recommended solution is to move padding to a child element. See this issue for the full discussion.

Type error with AnimateSharedLayout

AnimateSharedLayout was deprecated in 5.0. Refer to the upgrade guide for instructions on how to remove.

Preact isn’t working

Framer Motion isn’t compatible with Preact.

AnimatePresence isn’t working

Have all of its immediate children got a unique key prop that remains the same for that component every render?

// Bad: The index could be given to a different component if the order of items changes
<AnimatePresence>
  {items.map((item, index) => <Component key={index} />)}
</AnimatePresence>
// Good: The item ID is unique to each component
<AnimatePresence>
  {items.map((item, index) => <Component key={item.id} />)}
</AnimatePresence>

Is the AnimatePresence correctly outside of the controlling conditional? AnimatePresence must be rendered whenever you expect an exit animation to run - it can’t do so if it’s unmounted!

// Bad: AnimatePresence is unmounted - exit animations won't run
{isVisible && (
  <AnimatePresence>
    <Component />
  </AnimatePresence>
)}
// Good: Only the children are unmounted - exit animations will run
<AnimatePresence>
  {isVisible && <Component />}
</AnimatePresence>

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 30
  • Comments: 88

Commits related to this issue

Most upvoted comments

I was using old architecture of pages in Next-13.4. This article helped me solved it: https://blog.logrocket.com/advanced-page-transitions-next-js-framer-motion/#adding-animatepresence

The issue was adding key={router.pathname} in the _app.tsx and it worked.

import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import Header from "@/containers/Header";
import Footer from "@/containers/Footer";
import { ClerkProvider } from "@clerk/nextjs";
import Script from "next/script";
import { AnimatePresence, motion } from "framer-motion";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter"
});

const variants = {
  initialState: { opacity: 0 },
  animateState: { opacity: 1 },
  exitState: { opacity: 0 }
};

export default function App({ Component, pageProps, router }: AppProps) {
  return (
    <>
      <Head>
        <title>NextJS 13 | Learn NextJS 13 with old architecture</title>
        <meta
          name="description"
          content="Learn NextJS 13 pages directory architecture"
        />
      </Head>

      {/*<Partytown debug={true} forward={["dataLayer.push"]} />*/}
      <Script id="partytown-gtm" strategy="afterInteractive" dangerouslySetInnerHTML={{
        __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','XXX');`
      }} />
      <ClerkProvider {...pageProps}>
        <Header />
        <AnimatePresence initial={false} mode="wait">
          <motion.main
            initial="initialState"
            animate="animateState"
            exit="exitState"
            transition={{
              duration: 0.2
            }}
            variants={variants}
            key={router.pathname}
            className={`${inter.variable} font-inter`}>
            <Component {...pageProps} />
          </motion.main>
        </AnimatePresence>
        <Footer />
      </ClerkProvider>
    </>
  );
}

Here’s my experimental branch of next.js that enables exit transitions with AnimatePresence by adding in a custom Glue component (supplied in a glue.tsx) where we can monitor the lifecycle of routes:

Draft PR: https://github.com/vercel/next.js/pull/56591 Discussion: https://github.com/vercel/next.js/discussions/56594

The final solution in next.js is going to have to inevitably be something of this sort, as this enables route transitions even within parallel routes (where you can’t rely on pathname or global navigation events to perform the transitions).

Closing here as framework-specific integrations isn’t something I’m going to concentrate on this year.

I got exit animation to work using appDir (13.2.2-canary1). template.tsx is a lie, so ignore that until they fix it. What you want to do is in your layout.tsx (which is ideally rsc), wrap a Client component around your {children} and have that be wrapped in the <AnimatePresence> component.

// client.tsx
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'

// Client wraps any client/rsc components with AnimatePresence
export default function Client({children}: { children: ReactNode}) {
  const pathname = usePathname()
  return <AnimatePresence mode='wait' onExitComplete={doYourThing}>
     <motion.div key={pathname}
          initial={{}}
          animate={{}}
          exit={{}}
      >
        {children}
     </motion.div>
   </AnimatePresence>
}
// layout.tsx, this should ideally be `rsc`, so don't use `use client`
import Client from './client'
export default function Layout({children}: { children: ReactNode}) {
  return <Client>{children}</Client>
}


It turned out to start the animation of entry and exit. Maybe someone will be useful šŸ˜…

https://github.com/AksRK/next13-page-transition

Wow, four months in and still not a fix. Next.js devs are sleeping.

Ive made some progress on this. I still cannot get exit animations to work, but this snippet works well for enter animations that are pretty smooth. The only problem is that these dont load when the server is initial SSRing the page. after its cached it works fine. In the video, the documents page SSR’s on the first click, then after that you get decently smooth transitions.

// app/template.tsx
'use client';

import clsx from 'clsx';
import { useEffect, useState } from 'react';

export default function Template({ children }: { children: React.ReactNode }) {
  const [fade, setFade] = useState(false);
  useEffect(() => {
    setFade(true);
    return () => {
      setFade(false);
    };
  }, []);
  console.log(fade);
  return (
    <div
      className={clsx(
        fade ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-0',
        'transition duration-500 ease-out',
      )}
    >
      {children}
    </div>
  );
}

https://github.com/framer/motion/assets/34988548/f18dec5d-ec75-40f5-8738-892349efb3a2

@SchmidtDavid I am sorry I didn’t follow up with Next.js/Vercel in part because the app directory was in rapid flux/beta at that time. There were several known instabilities and incompatibilities with the wider React ecosystem.

I was hoping that the framer motion team (@mattgperry ) would have more direct channels & bandwidth to look into it.

That said, given the traction this has gained, and that the app directory is emerging/has emerged from beta, I am more than happy to follow up.

Here. I have filed a bug with Next.js.

In the meantime, for some of these scenarios, a possible workaround is to simply use CSS transitions, or the gorgeous view transitions now available in Chrome.

That is weird although I think I have them working here https://codesandbox.io/p/sandbox/charming-pine-zz12kx

Your example uses the pages directory. @maurocolella uses the new /app directory: https://beta.nextjs.org/docs. While still experimental it’s definitely the new way of creating Next apps.

There are seemingly a couple of issues with transitions. Most notable: the exit animations do not work (see video). I’m not sure whether the issue is with Framer or with Next. Next does seem to unmount the layout immediately upon navigation. I’ve tried adding usePresence to allow Framer to remove an element after a timeout but even that doesn’t work.

The props on the component doing the transition are:

initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 2 }}
key={pathname}

https://user-images.githubusercontent.com/6362631/213143210-634ce3fc-49ad-4135-b92a-9b1fe8aaa88d.mov

still doesn’t work

@alainkaiser it’s seeming like this is an issue on the Next.js side that will need to be fixed: https://github.com/vercel/next.js/issues/49279

Yes, I will put it on my list. I will link the bug here.

@khuezy in my instance pathname falls into an infinite loop and no route change happens

Hey folks, I’m running into the same issue here. I’m not sure how to even wrap AnimatePresence in NextJs’s new App directory because it’s throwing all kinds of issues. I’m combining it with styled-components and seems like there’s some stuff that’s happening there too.

Here’s how I’ve currently implemented my layout.js:

'use client'
import { AnimatePresence } from 'framer-motion'
import { GridOverlay } from '../components/GridOverlay'
import Header from '../components/Header'

import StyledComponentsRegistry from './styles/Registry'
import { ThemeContext } from './styles/ThemeContext'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>
          <ThemeContext>
            <AnimatePresence mode="popLayout">
              <GridOverlay />
              <Header />
              {children}
            </AnimatePresence>
          </ThemeContext>
        </StyledComponentsRegistry>
      </body>
    </html>
  )
}

One of the biggest errors I’m getting is:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `PopChild`.
    at OuterLayoutRouter (webpack-internal:///./node_modules/next/dist/client/components/layout-router.js:18:11)
    at PopChildMeasure (webpack-internal:///./node_modules/framer-motion/dist/es/components/AnimatePresence/PopChild.mjs:13:1)
    at PopChild (webpack-internal:///./node_modules/framer-motion/dist/es/components/AnimatePresence/PopChild.mjs:33:21)
    at PresenceChild (webpack-internal:///./node_modules/framer-motion/dist/es/components/AnimatePresence/PresenceChild.mjs:15:26)
    at AnimatePresence (webpack-internal:///./node_modules/framer-motion/dist/es/components/AnimatePresence/index.mjs:72:28)
    at Fe (webpack-internal:///./node_modules/styled-components/dist/styled-components.browser.esm.js:30:17299)
    at ThemeContext (webpack-internal:///./app/styles/ThemeContext.js:16:11)
    at StyledComponentsRegistry (webpack-internal:///./app/styles/Registry.js:19:11)
    at body
    at html
    at RootLayout (webpack-internal:///./app/layout.jsx:18:11)
    at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:53:9)
    at HotReload (webpack-internal:///./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:19:11)
    at Router (webpack-internal:///./node_modules/next/dist/client/components/app-router.js:96:11)
    at ErrorBoundaryHandler (webpack-internal:///./node_modules/next/dist/client/components/error-boundary.js:28:9)
    at ErrorBoundary (webpack-internal:///./node_modules/next/dist/client/components/error-boundary.js:40:11)
    at AppRouter
    at ServerRoot (webpack-internal:///./node_modules/next/dist/client/app-index.js:113:11)
    at RSCComponent
    at Root (webpack-internal:///./node_modules/next/dist/client/app-index.js:130:11)

Thank you @Frumba and @Gaurav4604 — that worked, thank you for the quick reply.

Hi everyone, I followed this thread and came across a hacky solution, It seems to work well for me

Check it out, might be useful.

using

  • nextJS (v 13.4.19)
  • framer-motion (v 10.16.4)

The thread itself has a working example, but I’ve implemented a solution with much less code as well.

My working example (it is a frontendmentor.io challenge solution)

Repo link

This is the FrozenRouter implementation that makes exit animations possible

Here is where the FrozenRouter component is being used

I hope this is helpful to all facing this issue.

Having the same problem with the new app directory, a thing I noticed is that it’s exclusively the exit animation bc I also have the ā€œinitialā€ prop set to false in AnimatePresence and that works hahaha

`export default function RootLayout({children }) { return (

<html lang="en">
  <body className={inter.className}>
    <Navbar />
    <AnimatePresence type='wait' initial={false}>
    {children}
    </AnimatePresence>
  </body>
</html>

) }`

Not sure if it’s a Framer motion or Next issue at this point though šŸ˜…

It does work but you have to be a bit creative with it. Here’s my solution.

  1. First, create a client component which will be the wrapper for Framer Motion’s AnimatePresence.
// LayoutAnimatePresence.tsx

'use client'

import { AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
import { Fragment, type PropsWithChildren } from 'react'

export default function LayoutAnimatePresence({ children }: PropsWithChildren) {
  const pathname = usePathname()

  return (
    <AnimatePresence initial={false} mode="wait">
      <Fragment key={pathname}>{children}</Fragment>
    </AnimatePresence>
  )
}
  1. Then, use it in your root layout.
// layout.tsx

import type { PropsWithChildren } from "react"

export default function RootLayout({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <body>
          <LayoutAnimatePresence>{children}</LayoutAnimatePresence>
      </body>
    </html>
  )
}

I have the same kind of issue in the Next 13. The Animate Presence is unable to freeze the exiting (children) component.

In the Root Layout, which is a server component, I have this: image

In Main (client component) I have this: image

In Inner (client component as well): image

I added Freeze(react-freeze) as a workaround, but I don’t like it.

It seems that both (the new component + the existing one) point somehow to the same component.

still doesn’t work

I can second this 😭

That being said I was able to get it working without an exit animation by using the motion tag as the root tag on each page (i.e., home, about, contact) and not using mode=ā€˜wait’ with AnimatePresence.

It looks like appDir is stable as of 13.4 today. Looking forward to getting a fix on this soon

I also give a try on the new app folder and indeed I have trouble with the exit animation, it just don’t work in the new App even with the template.js and separate it as a client component, however, in the pages folder it works fine.

@dumbravaandrei22 I’m using 9.0.3, I’ll update to latest and let you know. Although the example above gets exit animations to work, the current problem is that nextjs replaces the contents before the exit animation plays. I believe once Vercel fixes template.tsx, it will behave correctly. Edit: 10.0.1 exit animations work, but the premature navigation is still a problem. Lee from Vercel is aware of it so maybe it’s on their plate.

Just a follow up, I’m trying this approach in Next 13.5 which just came out today and I’m finding that the any components underneath a Frozen Router won’t update via HMR which makes it pretty much unusable.

@cdebotton I just updated my import for frozenRouter for it to work with AnimatePresence on nextJS 13.5.1, You can find it here but I get it, this needs a much more permanent fix, since these are not publicly exported components from nextJS.

Just a follow up, I’m trying this approach in Next 13.5 which just came out today and I’m finding that any components underneath the Frozen Router Component won’t update via HMR which makes it pretty much unusable.

Framer motion devs dont want to fix this? like, its been months now

This is a direct issue with next, and not framer. Once this issue is resolved it should work as indeed.

+1 same problem, would love to see a fix for exit animations, tried on latest Next canary release (13.3.2-canary.6) and still not working

I haven’t found a related ticket on the nextjs repo. Maybe we can create w/ some a minimal repro?

Ah I see, sorry missed your comment up there. We have the exact same problem.

I have it working for like 90%. The animation on Exit is or too fast loading animate-in or the exit is is too fast or too slow. The exit and enter works… but still works better using ā€œpagesā€ instead of the new ā€œappā€ directory.

layout.tsx:

"use client";

import "@styles/globals.css";

import { anton, roboto } from "lib/fonts";

import { Footer, MainNavigation, MaxWidthWrapper } from "@components/ui";
import { AnalyticsWrapper, DefaultHead } from "@components/shared";
import Newsletter from "@components/sections/Newsletter";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";

export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  const pathname = usePathname();

  return (
    <html lang="en" className={`${anton.variable} ${roboto.variable}`}>
      <DefaultHead />
      <body className="min-h-screen text-gray-100 bg-midnight">
        <MainNavigation />

        <MaxWidthWrapper type="main">
          <AnimatePresence
            mode="wait"
            initial={false}
            onExitComplete={() => window.scrollTo(0, 0)}
          >
            <motion.div
              key={pathname}
              initial={{ opacity: 0, y: 25 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: 25 }}
              transition={{
                type: "spring",
                stiffness: 140,
                damping: 20
              }}
            >
              {children}
            </motion.div>
          </AnimatePresence>
        </MaxWidthWrapper>

        <AnalyticsWrapper />
        <Newsletter />
        <Footer />
      </body>
    </html>
  );
}

Another finding: When using <AnimatePresence> in template.tsx, the onExitComplete() does not fire.

After reading the Next.js v13 docs regarding the new Templates feature, I tried it out to house page-transition Framer Motion code.

Unfortunately the exit animation does not work (it’s just completely ignored).

This is the file that contains my page-transition code.

Also see my template.tsx and layout.tsx file for more context.

Per https://github.com/framer/motion/discussions/1775#discussioncomment-4299578 I have also tried using a template.tsx file but that doesn’t seem to work either, even though the Next docs specifically mention transitions to be a use-case for it: https://beta.nextjs.org/docs/routing/pages-and-layouts#templates.

Hi there, that source code link isn’t working for me. Please reopen with a sandbox or repo