-
Notifications
You must be signed in to change notification settings - Fork 27k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NEXT-1151] App router issue with Framer Motion shared layout animations #49279
Comments
As an extra point for this issue, the docs specify that using templates is a way to achieve enter/exit animations (web archive link, see edit) with either CSS or an animation library (for which framer-motion would be one of the go-to choices for most I feel) but doesn't offer any indication as to how to achieve this. With templates, even if you wrap children in layout (where templates is rendered, and even given a key) transitions just don't work because there's no way to make that template a framer motion element with animate props. Edit: This wording is now removed, just linking the docs as they were at this moment in time so it doesn't look like I was saying the docs say something it doesn't: web archive link |
I believe I've pinpointed the issue that is causing this problem. As part of the new app router structure, Next lays out a tree of components that includes the following loop (rendering a hypothetical
Crucially, the app framework is inserting an Because the The potential fixes I see for this issue are:
I don't think there is a way with the APIs currently exposed to solve this issue on Framer's side without at least some change from Next. |
Just adding a +1 for this to be looked at soon, there's a lot of conversation about this in the Next.js discord as well |
Would also love if you guys could have a look at it soon. A good amount of information already present in the framer-motion thread: |
@seantai @jamesvclements @alainkaiser Please do not ping the thread with comments that do not add value. The issue is already synced into our tracker, there's hundreds of issues to be investigated and spamming issues demanding for it to be looked at is not the way to get us to look into it any faster, on the contrary, by posting these comments you're actively taking time away from us investigating / fixing issues. If all you want to do is "increase priority" you can use the 👍 on the initial post (not this post) to convey that you're running into it too. Or you can focus your efforts on investigating / providing context on what might be causing the issue like the great comment @zackdotcomputer added. |
Wouldn't an easier approach simply be to let us choose where to put the As an example right now two different pages that return the same component in the same position will get remounted: // app/foo/page.js
export default function Page() {
return <Counter />;
}
// app/bar/page.js
export default function Page() {
return <Counter />;
} These two counter's state will be lost when soft-navigating between By removing the // app/foo/page.js
export default function Page() {
const pathname = usePathname();
return <Counter key={pathname} />; // <-- don't preserve the counter state on soft-navigations
} This would of course allow us to properly adjust exit-animations, as well as other more fine-grained things we want to happen when soft-navigating. |
Hello, |
I have a question, I'm new to using nextjs framework. So It's my first time using an app router but I don't know how to use framer motion in app router. |
You can't correctly use framer motion for layout animations with app router for now |
Apart from layout animations, Framer Motion works perfectly fine in client components. |
Yeah to clarify, @harshv1741 - if you're looking to use Framer Motion to perform page transitions as the user navigates from page route to page route in app router, then that is what this issue is saying is broken. Because of how the NextJS team have structured their layouts feature, you can't do that right now. If you're looking for how to use Framer Motion inside of page or specific component, then that is out of the scope of this thread to help you with - I'd suggest taking that over to Framer Motion's site and community. |
Hi, is there any update? Or at least someone knows of another way to make an exit animation without using framer-motion? |
Hello, I just wanted to add that as far as I understand, Framer Motion as well as React Transition Group use React's Here is how a simple exit-before-enter-animation looks like in vanilla Remix (and I'd expect it to work similarly with the App Router in the future): export default function Layout() {
const outlet = useOutlet();
const [cloned, setCloned] = useState(outlet);
const href = useHref();
const mainRef = useRef();
useEffect(
function () {
mainRef.current.style.opacity = 0;
const timeout = window.setTimeout(function () {
mainRef.current.style.opacity = 1;
setCloned(cloneElement(outlet));
}, 500);
return function () {
window.clearTimeout(timeout);
};
},
[href],
);
return (
<>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/news">News</Link>
<Link to="/about">About</Link>
</nav>
</header>
<main style={{ transition: "opacity 500ms" }} ref={mainRef}>
{cloned}
</main>
</>
);
} |
I agree with @fweth. Even in my app using the pages router, the only thing I'm using framer motion for is page transition animations. It'll be amazing if a solution and examples can be provided for simple page transition animation for both the pages and app router. A solution with vanilla css / js / react will help reduce a good amount of bundle size. |
For animating modals using parallel routes, since we use Inside the modal component: 'use client';
import css from './Modal.module.scss';
import { useCallback, useRef, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';
export default function Modal() {
const router = useRouter();
const pathname = usePathname(); // to use pathname as motion key
const [show, setShow] = useState(true); // to handle mounting/unmounting
const onDismiss = useCallback(() => {
setShow(false);
setTimeout(() => {
router.back();
}, 200); // 200ms, same as transition duration (0.2)
}, [router]);
return (
<AnimatePresence>
{show && (
<motion.div
key={pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.2,
ease: 'easeInOut',
}}
>
{/* your modal content */}
</motion.div>
)}
</AnimatePresence>
);
} To see the complete example of creating modals with parallel routes (albeit without animations), check out Nextgram |
The workaround I'm using memorizes the Full example below: /// layout.tsx
function FrozenRouter(props: PropsWithChildren<{}>) {
const context = useContext(LayoutRouterContext);
const frozen = useRef(context).current;
return (
<LayoutRouterContext.Provider value={frozen}>
{props.children}
</LayoutRouterContext.Provider>
);
}
export default function Layout(props: PropsWithChildren<{}>) {
const pathname = usePathname();
return <AnimatePresence>
<motion.div
key={pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, type: "tween" }}
>
<FrozenRouter>{props.children}</FrozenRouter>
</motion.div>
</AnimatePresence>;
} This is working pretty good so far. Fingers crossed it works for you too! 🤞 |
Hey, I appreciate your help. Can you add a Repo Link to the working code example or provide more information about the Context Provider Configuration? |
I just managed to piece it together. Here's how I implemented the solution: import React, {useContext, useRef} from "react";
import { motion, AnimatePresence } from 'framer-motion';
import { PropsWithChildren, useRef } from 'react';
import { usePathname } from 'next/navigation'; // Import your pathname utility
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context";
function FrozenRouter(props: PropsWithChildren<{}>) {
const context = useContext(LayoutRouterContext);
const frozen = useRef(context).current;
return (
<LayoutRouterContext.Provider value={frozen}>
{props.children}
</LayoutRouterContext.Provider>
);
}
export default function Layout(props: PropsWithChildren<{}>) {
const pathname = usePathname();
return (
<AnimatePresence>
<motion.div
key={pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, type: 'tween' }}
>
<FrozenRouter>{props.children}</FrozenRouter>
</motion.div>
</AnimatePresence>
);
} |
Almost! The import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context"; I will try to share an example of how to use it with nextgram tomorrow. |
This actually works. Any reason why the API isn't public? |
A few questions we'd love to hear more feedback on here:
|
Let me know if I'm way off base here, but this thread - I don't think - has anything to do with things like nProgress. It's more like transitioning within a layout between routes. Easy to imagine with modal animations or fade-in and -out of previous to next route. Basically, all the example sites you see with the Chrome View Transitions API (but Framer Motion specifically) Essentially, in the pages router, the |
I found a website which uses app router and has done page transitions with shared layout transitions. Can someone tell me how? |
While fully acknowledging the tremendous effort invested in developing the app router, the possibility of building app-like experiences with the new model is enticing. Astro are setting a high standard with their view transitions api. I'm hoping that there's an official solution as this issue has persisted for a year |
Hey all, I built this website. I used this method from this thread, but my Here's the exact code for my frozen router. import { PropsWithChildren, useContext, useRef } from "react";
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
export function FrozenRouter(props: PropsWithChildren<{}>) {
const context = useContext(LayoutRouterContext);
const frozen = useRef(context).current;
return (
<LayoutRouterContext.Provider value={frozen}>
{props.children}
</LayoutRouterContext.Provider>
);
} Dependencies are Unsure if I can be any more helpful here, as this solution worked mostly fine for my use case, but I had to do some context provider magic for the persistent animation on the right-hand side of the website that is async from the main routes. |
This comment was marked as off-topic.
This comment was marked as off-topic.
Check this out (from no other than @shuding ) https://github.com/shuding/next-view-transitions/tree/main I have not tested it but following this issue closely. Almost all of my clients expect page transitions now. Sigh. My hunch is that @leerob et al. aren't sleeping on this but I couldn't pretend to know. |
CSS view transitions are not supported in Safari yet (next version). @shuding uses the same approach of overriding Link. Sam Selikoff used it too in his article. Although it works I dislike having to do that just to get view transitions. It is a lot of work and in this case there is no support for replace, and no checks for modifier keys so e.g. cmd+click for opening new tab won't work anymore. So in my opinion it's still a hack and not a good long term solution. |
Updated Page transition Animation demo using App Router. When integrating into App Router, I refer to the Frozen Router idea. Demo : https://mekuri.vercel.app/ Features 📕
|
Are Shared layout animations possible? I can't get it to work😭 |
@paperpluto |
@rijk I am having an issue with your aproach, if I put a delay of 5seconds, the pending state last forever, i have to click again in the link and then it works, but this doesnt happends with 4 seconds for example 🤷♂️. I am working with Promis.all example |
And I dont understand why async callback works when react says that the function should be sync (except server actions) |
The solution by @rijk and other here worked for me but failed for my use case of nested transitions. I want to have a layout and transition just the sub page segment, and be able to nest these. I've modified it so it supports this:
Use like this:
Hope this helps someone out |
[Note: I am very new to both next.js and framer] I've found another potentially very "hacky" way as a work around for exit animations on page redirect. I created a client component that monitors the browser URL (using |
I found another way to make page transitions work with the app router. It's a bit hacky, it uses the fact that "use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { usePathname } from "next/navigation";
export default function Transition({ children }) {
const [exiting, setExiting] = useState(false);
const path = usePathname();
const cloneRef = useRef();
const innerRef = useRef();
const outerRef = useRef();
useMemo(
function () {
if (!innerRef.current) return;
setExiting(true);
cloneRef.current = innerRef.current;
},
[path]
);
useEffect(
function () {
if (exiting) {
outerRef.current.appendChild(cloneRef.current);
cloneRef.current.style.transition = "none";
cloneRef.current.style.opacity = 1;
window.setTimeout(function () {
cloneRef.current.style.transition = "opacity 400ms";
cloneRef.current.style.opacity = 0;
}, 100);
window.setTimeout(function () {
setExiting(false);
cloneRef.current.remove();
}, 500);
return () => cloneRef.current.remove();
}
window.setTimeout(function () {
if (!innerRef.current) return;
innerRef.current.style.opacity = 1;
}, 100);
},
[exiting]
);
return (
<div ref={outerRef}>
{!exiting && (
<div
key={path}
ref={innerRef}
style={{ opacity: 0, transition: "opacity 400ms" }}
>
{children}
</div>
)}
</div>
);
} |
Was running into a similar issue with page router not updating /* motion-div-reveal.tsx */
'use client';
import {DynamicAnimationOptions, HTMLMotionProps, useAnimate} from 'framer-motion';
import {PropsWithChildren, useEffect} from 'react';
export type MotionDivRevealProps = HTMLMotionProps<'div'>;
// This is a basic replacement for <motion.div> in scenarios where the app router is used to navigate
// between components that have an entry animation.
// This is a workaround for this issue:
// https://github.com/vercel/next.js/issues/49279
export const MotionDivReveal = (props: PropsWithChildren<MotionDivRevealProps>) => {
const [scope, animate] = useAnimate();
useEffect(() => {
if (!scope.current) {
return;
}
let containerKeyFrames: Record<string, any[]> = {};
// Check if props.initial is a boolean type
if (props.initial instanceof Object && props.animate instanceof Object) {
// eslint-disable-next-line guard-for-in
for (const key in props.initial) {
// @ts-expect-error any type is inferred for this keys/values
containerKeyFrames[key] = [props.initial[key], props.animate[key] ?? props.initial[key]];
}
} else {
console.warn('MotionDivReveal: initial and/or animate prop is not an object, skipping animation.');
return;
}
void animate(
scope.current,
containerKeyFrames,
props.transition ?? {
bounce: 0,
duration: 0.3, /* 300ms */
},
);
}, [/* no dependencies to ensure the animation only runs once, or is skipped if the scope is not set */]);
return (
<div ref={scope} className={props.className}>
{props.children}
</div>
);
}; Then, this can be used like you would have used a <MotionDivReveal
initial={{opacity: 0, x: -50}}
animate={{opacity: 1, x: 0}}
>
Watch me slide in
</MotionDivReveal> This works because it fires the animations with a |
@lochie Just curious, have you had any issues with using the FrozenRouter method in production? Would love to use page transitions as apart of my client projects and personal projects but I'm afraid there might be drawbacks to using this method. Fingers crossed that the Next.js team will be tackling this real soon, we all have been eagerly waiting for an official solution 🥹 |
@huyngxyz there are different issues for styling depending on what styling solution you use. i know we had issues with (s)css modules and had to implement an unmount delay for component styles, and styled-components also had some issues. tailwind might be okay 🤷♀️ there were definitely more issues than not, it was enough that i considered switching back to page router multiple times during development, and i still generally prefer to opt for page router when it comes to having animation-rich web apps depending if the app would benefit hugely from server components. it's all a balancing act. |
@lochie Awesome, thanks for sharing! Gonna give it a try and see how it's like with tailwind |
is there any way to start |
is there any news when this bug will be fixed in the app router? How to implement |
I was able to get exit transitions working thanks to #49279 (comment). Couldn't figure out why they weren't working at first even after implementing the code from the above comment; turns out, you have to use Next.js's Link component rather than standard HTML <a> elements, or it won't work. Not sure why I wasn't using Link components to being with, but I figured I would leave this here in case someone else runs into the same issue. |
Yes it works that way or using router from next/navigation. Still, for intercepting modals, exit transitions don't work since the close button uses router.back() ... My work around was to save the previous path in a state management and then use it as the redirect path when the modal closes, but it's not a very good approach. |
Yes it works that way or using router from next/navigation. Still, for intercepting modals, exit transitions don't work since the close button uses router.back() ... My work around was to save the previous path in a state management and then use it as the redirect path when the modal closes, but it's not a very good approach. |
this solution works for me
warning only appears when refreshing, it doesn't appear when navigating through pages |
try this frozen router function FrozenRouter(props: { children: React.ReactNode }) {
const context = useContext(LayoutRouterContext ?? {});
const frozen = useRef(context).current;
if (!frozen) {
return <>{props.children}</>;
}
return (
<LayoutRouterContext.Provider value={frozen}>
{props.children}
</LayoutRouterContext.Provider>
);
} |
Verify canary release
Provide environment information
Operating System: Platform: linux Arch: x64 Version: #22 SMP Tue Jan 10 18:39:00 UTC 2023 Binaries: Node: 16.17.0 npm: 8.15.0 Yarn: 1.22.19 pnpm: 7.1.0 Relevant packages: next: 13.4.1-canary.1 eslint-config-next: 13.0.7 react: 18.2.0 react-dom: 18.2.0
Which area(s) of Next.js are affected? (leave empty if unsure)
App directory (appDir: true)
Link to the code that reproduces this issue
https://codesandbox.io/p/sandbox/stupefied-browser-tlwo8y?file=%2FREADME.md
To Reproduce
I provided a larger repro for context, as it is unclear which combination of factors leads to the specific bug, although a number of other people report the same issue.
Describe the Bug
Framer Motion supports a feature called shared layout animation that automatically transitions components whose styles have changed when the container (that contains them) re-renders.
This feature appears not to be working in multiple scenarios with Next.js 13 under the app folder.
In the provided example, this feature is applied to the blue navigation highlight.
The affected container in the code sandbox is:
https://codesandbox.io/p/sandbox/stupefied-browser-tlwo8y?file=%2Flib%2Fcomponents%2FNavigation.tsx
To produce the undesired behavior, I simply applied
layoutId
as specified in the relevant Framer Motion documentation to the motion elements expected to transition.I believe I also tried more explicit variations. Others have reported similar or identical issues in the bug currently open with Framer Motion.
Expected Behavior
I expect the blue highlight to slide smoothly to its new position when the nav container re-renders.
Which browser are you using? (if relevant)
Version 113.0.5672.63 (Official Build) (64-bit)
How are you deploying your application? (if relevant)
Usually Vercel
NEXT-1151
The text was updated successfully, but these errors were encountered: