framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
22.56k stars 748 forks source link

Can't get shared layout animations to work with Next.js 13 #1850

Closed maurocolella closed 5 months ago

maurocolella commented 1 year ago

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>
thanhtutzaw commented 1 year ago

yo I also have this issue in Next js . I solved it for my first time use but , I deleted that animation . Now I want to enable in next js and issue happening .

mattgperry commented 1 year ago

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

maurocolella commented 1 year ago

@mattgperry was private, sorry.

Here is the sandbox:

https://codesandbox.io/s/hopeful-wozniak-hkf8yx

mattgperry commented 1 year ago

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

mattgperry commented 1 year ago

When I've got a bit more time I can look into this, there's definietly something different about Next 13

maurocolella commented 1 year ago

Thanks a lot @mattgperry . It's early days for this project, and it's going to undergo some changes (maybe Remix, some css tweaks, and I will definitely lose the cheerio parsing once I connect it with a CMS back-end), but I thought I'd raise it as an edge case.

I am not blocked by all means, and if I figure out why it's working this way I'll be happy to share back.

Appreciate the help.

MrUltimate commented 1 year ago

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)
Murkrage commented 1 year ago

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

Murkrage commented 1 year ago

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.

maurocolella commented 1 year ago

Worth noting that I have organized my client/server components in a peculiar way to work around apparent bundling issues.

This is related to cheerio as mentioned (which I will drop soon).

That being said, I don't think I am working against either framework, and I might raise an issue with the Next team for their feedback.

On Wed, Jan 18, 2023, 6:20 PM Mike Ekkel @.***> wrote:

Per #1775 (reply in thread) 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.

β€” Reply to this email directly, view it on GitHub https://github.com/framer/motion/issues/1850#issuecomment-1386815010, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFZ7NBNFOOSD6OXYA5ZNQDWS67YFANCNFSM6AAAAAATJVFL2I . You are receiving this because you were mentioned.Message ID: @.***>

dumbravaandrei22 commented 1 year ago

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.

Murkrage commented 1 year ago

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

This is, presumably, solved using template.js (see: https://beta.nextjs.org/docs/routing/pages-and-layouts#templates). From the docs:

...routes that share a template, a new instance of the component is mounted, DOM elements are recreated...

jasonkylefrank commented 1 year ago

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.

jasonkylefrank commented 1 year ago

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

dumbravaandrei22 commented 1 year ago

@Murkrage Some time ago I tried to use template.js but it didn't work. i will try again.

@jasonkylefrank For me the exit animation works. Try to wrap your motion.div into another component (client component). For that component provide key=pathname as you do. See my structure here: https://github.com/framer/motion/issues/1850#issuecomment-1418031574

in the inner component please use: const [isPresent, safeToRemove] = usePresence(); My next js version: ^13.1.7-canary.4

Now, the problem is that the old component and the new one are pointing to the same instance (they are the same component). I also find that the old component sometimes it doesn't get unmounted.

https://user-images.githubusercontent.com/14004579/217095185-3c52f30b-ce8e-4ac2-9063-dc2b294e8a8f.mov

Galanthus commented 1 year ago

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>
  );
}
khuezy commented 1 year ago

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>
}
beamercola commented 1 year ago

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

khuezy commented 1 year ago

@beamercola hmm, what does your app structure look like? My solution is a hack until they fix template.tsx.

dumbravaandrei22 commented 1 year ago

@khuezy What is your framer-motion version? I still didn't manage to solve this issue. I am using Next 13.2.2-canary1 and framer-motion: ^10.0.1. I did exactly what you did.

khuezy commented 1 year ago

@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.

dumbravaandrei22 commented 1 year ago

Understood! Thank you!

dumbravaandrei22 commented 1 year ago

@khuezy I tried this: https://github.com/framer/motion/issues/1850#issuecomment-1418031574. Is working, but it seems that the loading UI has a delay.

khuezy commented 1 year ago

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

dumbravaandrei22 commented 1 year ago

@khuezy Indeed, np. Should we file a new bug to NextJs? Or do you track a relevant bug related to this issue on the Vercel side?

khuezy commented 1 year ago

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

dumbravaandrei22 commented 1 year ago

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

akashlama1998-icloud commented 1 year ago

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>
  )
}
dumbravaandrei22 commented 1 year ago

@akashlama1998-icloud is the mode=wait required? Also, where do you provide the animation props?

NacereddineRebouh commented 1 year ago

@akashlama1998-icloud I tried your solution, and it works only in the page directory.

akashlama1998-icloud commented 1 year ago

@dumbravaandrei22 Updated code for LayoutAnimatePresence wrapper component for Next.js 13.

// LayoutAnimatePresence.tsx

'use client'

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

export default function LayoutAnimatePresence({
    children,
    ...restProps
}: PropsWithChildren<AnimatePresenceProps>) {
    const pathname = usePathname()

    return (
        <AnimatePresence {...restProps}>
            <Fragment key={pathname}>{children}</Fragment>
        </AnimatePresence>
    )
}
akashlama1998-icloud commented 1 year ago

@NacereddineRebouh You're right! I was about to post this it seems like exit animations still doesn't work inside the new app directory if wrapped around to the whole children layout. May be any update by @mattgperry

NacereddineRebouh commented 1 year ago

@akashlama1998-icloud a few minutes ago, I had the onExitAnimation fire, and the pages console log when unmounting, but still, the exit animation does not work; I even tried using template.tsx. something weird is happening with nextjs. I had two pages: /page1 and /page2 -each one with a use effect that console log a message when mounting and unmounting -I used your LayoutAnimatePresence in Layout.tsx, -and I had a Header with Two Link tags to page1 and page2 here's what I had in the console: page 1 mounted LayoutAnimatePresence mounted navigating to Page2....... page 1 unmounting page 2 mounted

everything works as expected but the exitAnimation.

akashlama1998-icloud commented 1 year ago

@NacereddineRebouh Yes the same thing is happening with me.. till the bug is reviewed and fixed I shifted my whole project to the old way using pages directory.. @mattgperry is the only faith and hope I have now.. πŸ₯²

ShueiYang commented 1 year ago

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.

okybr commented 1 year ago

Here's a minimal reproducing example for @khuezy's approach which shows bad exit-behaviour: https://github.com/okybr/mre-next13-appdir-framer-motion

atabv commented 1 year ago

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

Pipoteex commented 1 year ago

Hi i have the same problem in Next 13. The exit animation not work if the AnimatePresence component is in the layout root component

willhindson commented 1 year ago

+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

Dimitri-Jacquin commented 1 year ago

also can't get it to work, it's a shame because the latest version of Next.js is very easy to use

willhindson commented 1 year ago

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

jamesvclements commented 1 year ago

Any updates on this? In the latest versions of framer motion 10.12.4and next 13.3.2-canary.12, the exit animation is working, but the premature navigation that @khuezy mentioned is still happening, which causes a weird flash of the next page's content before the animation's trigger:

https://share.cleanshot.com/KmksdBxN

That being said I'm still using layout.tsx. If switching to template.tsx, do we lose all the benefits of SSR? Or will the page.tsx still be SSR'd, and only the root layout misses that?

jamesvclements commented 1 year ago

as a follow-up to the above, I tested switching to using template.tsx and the exit animation no longer works

alainkaiser commented 1 year ago

Any updates on this from anyone? Would be helpful...

nohr commented 1 year ago

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

SchmidtDavid commented 1 year ago

Is there also an issue for this open somewhere on Nextjs's repo or is this suposed to stay on the framer repo?

maurocolella commented 1 year ago

@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.

maurocolella commented 1 year ago

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.

Dimitri-Jacquin commented 1 year ago

Thank you maurocolella for this great idea, could you please provide more details about your proposal? Like adding CSS classes with effects is that your idea? And why does the patterns-view transitions doesn't work in all browsers? How to use it with next?

maurocolella commented 1 year ago

@Dimitri-Jacquin all the effects that can be achieved using Framer Motion build up on underlying APIs, like CSS transitions yes or CSS animations, or the Web Animations API.

So any of these solutions can be used to work around the temporary misalignment between Framer Motion and Next.js. I am sure you can find tutorials for any of them easily for quite a large number of effects.

As for view transitions, it's a relatively new API in the exploratory stages. It has yet to become a standard, but it's on the way. The article I linked has details about how to use it with Next.js. Just scroll down under the React section, although you can find other great tutorials for that as well.