Closed MatteoGauthier closed 10 months ago
In _app.tsx
, you need to wrap your page component with an AnimatePresence
like so:
<AnimatePresence exitBeforeEnter>
<Component {...pageProps} key={router.pathname} />
</AnimatePresence>
See: https://wallis.dev/blog/nextjs-page-transitions-with-framer-motion
This used to work well with older versions of next.js, but doesn't work anymore. I can't figure out why.
I struggled to get this working as well. However, I was able to get it to work after correctly with Next v12.0.7 specifying a key for the component as suggested by @MarcGuiselin.
export default function App({ Component, pageProps, router }) {
return (
<AnimatePresence exitBeforeEnter>
<Component {...pageProps} key={router.pathname} />
</AnimatePresence>
);
}
https://www.framer.com/docs/animate-presence/##animating-custom-components
I'm relived that all my nested staggerChildren
transitions in children seem to still be working! 👍🏻
Same issue here =/
Thanks a lot, it fixed the issue.
When I wrap
`
`
It doesn't work for me on Next's latest version
@teauxfu solution resolved this for me on 12.0.8.
<Header />
<AnimatePresence
exitBeforeEnter
initial={false}
onExitComplete={() => window.scrollTo(0, 0)}
>
<Component {...pageProps} key={router.pathname} />
</AnimatePresence>
<Footer />
Ok, it's an issue with React 18 (concurrent mode), see https://github.com/framer/motion/issues/1421
you can refer to this example from official documentation of Nextjs
https://github.com/vercel/next.js/blob/canary/examples/with-framer-motion/pages/_app.js
lately, i don't know why animation presence don't work like expected in the initial animation?
the exit animation is not working in dynamic routes
@aluku7-wq Working for me, but be sure to pass the key as router.asPath
, otherwise it's "/articles/[slug]"
<Component {...pageProps} key={router.asPath} />
Hello! I have the same problem but it only happens in some transitions. I have (an example) the following routes: /projects.js /journal.js /[....slug].js ---> /studio, /legal...
I have my app.js as the comment above, wrapped in LazyMotion with domAnimation and AnimatePresence:
<LazyMotion features={domAnimation}>
<AnimatePresence
exitBeforeEnter
onExitComplete={() => {
console.log("EXIT COMPLETE", router.asPath);
}}
>
<Component {...pageProps} key={router.asPath} />
</AnimatePresence>
</LazyMotion>
When I navigate from /projects.js to /journal.js onExitComplete runs, but when I try to navigate from any of those two pages to one of [...slug.js] onExitComplete does not run.
Still, when I navigate between pages from [...slug.js] (from /studio to /legal) the transition works, so I am quite confused as to why this is happening.
Since my app is pretty complex by now (localized routes, caches, styled components) I don't know how I could make a sandbox to give an example, but maybe someone has any idea of what is happening.
Thank you all!
Okay, I found the cause. I had my _app.js main component wrapped in appWithTranslation HOC from next-i18next and this broke the transitions.
Do any of you guys have any idea how to fix this?
@cristobalbahe did you find any solution for that? I am experiencing the same.
@cristobalbahe did you find any solution for that? I am experiencing the same.
Hey @f4z3k4s, I managed to fixed it by changing from next-i18next to next-intl. I guess since next-intl uses a regular component (
@cristobalbahe Thanks for the answer. Since then, I've bypassed the issue by implementing a routing animation myself without AnimatePresence with the help of router events. Then, we know it's probably an issue with next-18next based on your solution.
Done some extensive testing all options here. It's not isolated to next-i18next.
I DO not get the issue if I have a [id].tsx I ALWAYS get the issue if I have [...slug].tsx
Something about the spread operator in the page name makes it happen.
The solution from @teauxfu and @MarcGuiselin works flawlessly for me. Using React 18.1.0 & Next 12.1.6.
Thanks for the knowledge
Having the same problem in nextjs 13.0.5 and react 18.0.2 with framer-motion 7.6.12; none of the previous solutions have been helpful. onExitComplete never triggers at all
@iamfrisbee W/ Next 13.0.5 + React 18.2.0 + framer-motion 7.6.17 I was able to get page entrance & exit animations to work w/ this snippet:
<AnimatePresence
mode="wait"
initial={false}
onExitComplete={() => window.scrollTo(0, 0)}
>
<Component {...pageProps} key={router.pathname} />
</AnimatePresence>
@MattWIP
hi, have you tried with next13 app directory? I do the same but still not working
@iamfrisbee W/ Next 13.0.5 + React 18.2.0 + framer-motion 7.6.17 I was able to get page entrance & exit animations to work w/ this snippet:
<AnimatePresence mode="wait" initial={false} onExitComplete={() => window.scrollTo(0, 0)} > <Component {...pageProps} key={router.pathname} /> </AnimatePresence>
So I had to upgrade a few packages to get that, but no, it doesn't work. I verify this by putting a console.log in onExitComplete and it never runs.
I'm experiencing the same thing as @iamfrisbee - I tried many different ways yesterday, and none worked.
FWIW, commenters on this YouTube video also are running into the same thing.
Yeah we've tried everything here and still nothing. We're on Next 13.1.1, React 18.2.0 and Framer Motion 8.0.2. Fingers crossed this gets fixed soon!
Running into this issue as well
In case this isn't resolved soon and anyone else find this thread.. I found a hacky workaround for route animations thats compatible with the nextjs app directory feature.
const router = useRouter()
const controls = useAnimationControls()
const onRoute = useCallback((href: string) => async () => {
await router.prefetch(href)
await controls.start('exit')
await router.push(href)
await controls.set('hidden')
await controls.start('enter')
}, [router, controls])
<motion.main
animate={controls}
variants={{
hidden: { opacity: .3, x: -200, y: 0 },
enter: { opacity: 1, x: 0, y: 0 },
exit: { opacity: .3, x: 0, y: -100 },
}}
transition={{ type: 'keyframes', duration: 2 }}>
{children}
</motion.main>
<button onClick={onRoute('page-1')}>
Link
</button>
You can manually trigger enter/exit animations via the useAnimationControls
hook. Instead of using <Link />
I call the onRoute
method and route once the exit animation has completed.
NOTE: this code only works in layout.tsx
components
Next: 13.1.1
framer-motion: 8.4.3
using the app
directory
Same issue. I will wait without exit animation until this is resolve.
So i've faced the same issue using next^12.1.0 & react^18.2.0 & react-dom^18.2.0 & framer-motion^7.6.4 Turns out when you wrap your Component in _app.js like this:
<AnimatePresence
onExitComplete={() =>
console.log("exit completed, pathname:", router.asPath)
}
mode="wait"
initial={false}
>
<Component {...pageProps} key={router.asPath} />
</AnimatePresence>
It works fine on static pages that catches multiple routes like [[...slug.js]], but struggles to remove the elements from the dom if its a regular [...slug.js].
Couldn't find a proper way to handle this but instead of wrapping the Component in _app.js, I wrapped the layouts in [[...slug.js]] files fixed the issue.
Done some extensive testing all options here. It's not isolated to next-i18next.
I DO not get the issue if I have a [id].tsx I ALWAYS get the issue if I have [...slug].tsx
Something about the spread operator in the page name makes it happen.
have the same issue, it also stated on the beta docs from nextjs to use templates.
I can see on react-devtools the key
is updating but the exit animation is not working
Mine is not working either, but on normal components, not even pages.
Check out my comment on https://github.com/framer/motion/issues/1850#issuecomment-1445239322, it enables exit animations on appDir
This work for me on _app.tsx
next 13.1.6 framer-motion 8.5.5
import { usePathname } from "next/navigation";
const pathname = usePathname();
<AnimatePresence
mode="wait"
onExitComplete={doSomething}
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
key={pathname}
>
<Component {...pageProps} />
</motion.div>
</AnimatePresence>
@hasolu everything works fine using the pages except using the new app dir and the layout template
@davidkhierl now i realize this code is not working in production, i'm dealing with this.
I'm having issues with this too. React 18.2, Framer Motion 10.0.1 and Next.js 13.2.3
Have y'all found any workarounds yet? I'm tried @joshdegouveia's solution but I kept getting Error: NextRouter was not mounted.
UPDATE: if you get that error, change next/router
to next/navigation
.
I got the workaround working, but I still can't use it with my navbar which is in the actual layout component just yet.
Also, seems like I'm getting the opposite error here. I can't make my intro animations work :sweat_smile:
The problem is the <AnimatePresence/>
should not experience any rerender, its either you try to create a wrapper before the layout
or wait nextjs to fix the template
file, this is where you will put this component, but upon checking from react dev tools the template component somewhat create another wrapper to it children hence it cannot access the key from the motion
componet which is required to let the exit animation to fire
I am still fighting with this!!!
Not even in non-dynamic pages and not using the new app dir structure - I can't get the exit animation to trigger nor the onExitComplete
callback to run! 😢
Dep versions:
"next": "^13.2.4",
"react": "^18.2.0",
"framer-motion": "^10.9.2"
Anyone cracked this already?
@psoaresbj The Exit Animation work in the page dir, did you give a try ?
@ShueiYang not super helpful. We all know the exit animations work with the page directory—the entire point of this issue is that they don't work with Next.js 13's app directory.
@harshhhdev my bad if i misunderstood when he said it's not working even when he don't use the new app dir, I am also fighting with the exit animation in the app dir that's why i am here.
Yes, in page dir, everything works fine!
appdir is officially stable, hope this solved soon
no idea what needs to happen to fix this, but Ill be super happy when app directory works with
I tracked down what I think is the issue with appdir - documented here https://github.com/vercel/next.js/issues/49596
TLDR; Next is sticking an unkeyed component between the layout
(which persists between pages) and template
(which does not). Because this means your only choices are the parent of an unkeyed element or something that will be cleared away on page navigation, there's nowhere to put the AnimatePresence
object to capture a page-exit for animation.
Whaat? Im having a hard time trying to figure out how to get it working in the page directory. I use "framer-motion": "^10.12.9", "next": "13.4.1", "react": "18.2.0"
index.jsx import Starterpage from '@/components/Starterpage/Starterpage.jsx' import Textani from '@/components/Starterpage/textani.jsx' import Dots from '@/components/Starterpage/Dots.jsx' import { motion } from 'framer-motion' import { useRouter } from "next/router";
import Head from 'next/head'
export default function Welcome(props) { const router = useRouter()
return (
<>
<motion.div
key={router.route}
initial="initialState"
animate="animateState"
exit="exitState"
transition={{
duration: 0.75,
}}
variants={{
initialState: {
opacity: 0
},
animateState: {
opacity: 1
},
exitState: {
scale : 0
}
}}
style={{width: '100%', minHeight: '100vh'}}
>
<Head>
<title>Welcome</title>
<meta name="description" content="Welcome To my portfolio page" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Starterpage>
<Textani word={props.randomWord} />
<Dots numdots={3} />
</Starterpage>
</motion.div>
</>
)
}
export async function getStaticProps(context) { const wordList = ['Hello', 'Konnichiwa', 'Bonjour', 'Guten', 'Ciao', 'Ola', 'Marhaba', 'Nǐn hǎo']
const randomWord = wordList[Math.floor(Math.random() * wordList.length)]
return {
props: { randomWord }
};
}
__app.js import '@/styles/globals.scss' import { AnimatePresence } from 'framer-motion'
export default function App({ Component, pageProps }) { return ( <AnimatePresence mode='wait' initial={false} onExitComplete={()=>{console.log('exit completed')}}> <Component {...pageProps} /> ) }
/home.jsx import Head from 'next/head' import Homepage from '@/components/Homepage/Homepage' import { motion } from 'framer-motion' import { useRouter } from "next/router";
export default function Home() { const router = useRouter()
return (
<>
<motion.div
key={router.route}
initial="initialState"
animate="animateState"
exit="exitState"
transition={{
duration: 0.75,
}}
variants={{
initialState: {
opacity: 0,
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0% 100%)",
},
animateState: {
opacity: 1,
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0% 100%)",
},
exitState: {
opacity: 0,
clipPath: "polygon(50% 0, 50% 0, 50% 100%, 50% 100%)",
},
}}
style={{width: '100%', minHeight: '100vh'}}
>
<Head>
<title>Home</title>
<meta name="description" content="Here you will some info about me" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Homepage />
</motion.div>
</>
)
}
I'm trying to make it work with the app directory as well, but can't seem to get an exit animation working properly.. Instead the elements are just removed without an animation. I thought the if statement within the AnimatePresence
should trigger the exit animation? This component is in the root layout.tsx
// PageWrapper.tsx
'use client';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';
export const PageWrapper = ({ children }: PageWrapperProps) => {
const pathname = usePathname();
const [isTransitioning, setIsTransitioning] = useState(false);
useEffect(() => {
setIsTransitioning(true);
setTimeout(() => {
setIsTransitioning(false);
}, 2000);
}, [pathname]);
const variants = (index: number) => ({
hidden: {
scaleY: 0,
transition: {
duration: 0.2,
delay: index * 0.1,
},
},
visible: {
scaleY: 1,
transition: {
duration: 0.2,
delay: index * 0.1,
},
},
});
return (
<AnimatePresence
initial={false}
onExitComplete={() => console.log('EXIT COMPLETE')}
>
{isTransitioning ? (
<>
<motion.div
key={`${pathname}_animation_1`}
variants={variants(1)}
initial="hidden"
animate="visible"
exit="hidden"
className="w-[50vw] h-[100vh] bg-rnny-primary fixed bottom-0 left-0 z-50"
/>
<motion.div
key={`${pathname}_animation_2`}
variants={variants(2)}
initial="hidden"
animate="visible"
exit="hidden"
className="w-[50vw] h-[100vh] bg-rnny-primary-tint fixed bottom-0 left-[50vw] z-50"
/>
</>
) : null}
{children}
</AnimatePresence>
);
};
What am I missing or not understanding?
EDIT:
Nvm, found it; Had to wrap my motion components in a <Fragment key="loading_animator">
with a key
, so "Framer Motion can track the AnimatePresence
" direct child presence.
@rnnyrk can you provide a code example of how you solved it?
@dnlaviv For full code see my repo
// layout.tsx
import './global.css';
import type * as i from 'types';
import { PageWrapper } from 'modules/layouts/PageWrapper';
const Layout = ({ children }: Props) => {
return (
<html lang="en">
<head />
<body>
<main>
<PageWrapper>{children}</PageWrapper>
</main>
</body>
</html>
);
};
type Props = i.NextPageProps<{
children: React.ReactNode;
}>;
export default Layout;
// PageWrapper.tsx
'use client';
import { Fragment, useEffect, useRef, useState } from 'react';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion, Variants } from 'framer-motion';
import { cn } from 'utils';
export const PageWrapper = ({ children }: PageWrapperProps) => {
const pathname = usePathname();
const [isTransitioning, setIsTransitioning] = useState(true);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setIsTransitioning(true);
timeoutRef.current = setTimeout(() => {
setIsTransitioning(false);
window.scrollTo(0, 0);
}, 1100);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [pathname]);
const variants: (index: number) => Variants = (index: number) => ({
hidden: {
scaleY: 0,
transition: {
duration: 0.4,
delay: index * 0.05,
ease: 'easeInOut',
},
},
visible: {
scaleY: 1,
transition: {
duration: 0.4,
delay: index * 0.05,
ease: 'easeInOut',
},
},
});
return (
<AnimatePresence>
{isTransitioning ? (
<Fragment key="route_transition_animator">
{[...Array(4).keys()].map((index) => {
return (
<motion.div
key={`${pathname}_animation_${index}`}
variants={variants(index)}
initial="hidden"
animate="visible"
exit="hidden"
/>
);
})}
</Fragment>
) : null}
{isTransitioning ? <div className="min-w-screen min-h-screen" /> : children}
</AnimatePresence>
);
};
type PageWrapperProps = {
children: React.ReactNode;
};
The exit animation seems to be determined by whether the element is unmounted or not. And AnimatePresence is too complex and it seems that the timing of the exit animation can't be customized, so I wrote a component to control when the motion animations are executed. Although it's not perfect, I'm going to use it in a production environment. It is an example of how it's used in CodeSandbox.
@rnnyrk Please, don't mind my question as I'm not a web dev per se - how does your solution relate to SSR? I guess when you wrap all your components in a component with use client
at the top, SSR is basically impossible, and this in turn impacts SEO.
@colonder This solution indeed doesnt support SSR. But there shouldn't be any hydration errors, because it's just an visual element on top of the current page. Next page loaded in the background. Didn't find a better solution yet. Open for one tho
1. Read the FAQs 👇
2. Describe the bug
I tried to integrate framer motion to next.js, I have components that appear on every page and when the road changes there is an animation.
3. IMPORTANT: Provide a CodeSandbox reproduction of the bug
https://codesandbox.io/s/github/MatteoGauthier/vertical-gallery/tree/52627524a54ee628bbde2f360c77b6d75c41593e
5. Expected behavior
Exit animation on route change
6. Video or screenshots
https://user-images.githubusercontent.com/32040951/144331637-d4ef82e2-2092-4b97-b156-1658e850763d.mov
Thanks