Next.js Discord

Discord Forum

Trying to use framer motion ("motion/react") with intercepted and parallel routes.

Answered
Austrian Black and Tan Hound posted this in #help-forum
Open in Discord
Austrian Black and Tan HoundOP
We have a "aside" parallel route which is responsible for displaying any content on client side navigation within an aside.
-app
---page.tsx
---layout.tsx
---@aside
-----default.tsx
-----(.cart)
-------page.tsx
---cart
-----page.tsx


The initial animations from framer work as intended when client side routing to the cart for the very first time. The motion components animate as intended. What's weird is if you use "router.back" and then go back to the cart route, no animation happens. Almost as if the aside route for "/cart" was never unmounted. I've tried manually unmounting it myself using:
const path = usePathname()
const inView === "/cart";

return inView && <motion.div>{/** other stuff here **/}</motion.div>


But that doesn't seem to work either, does anyone know of a working solution for this in v16 of Nextjs or a potential solution? I feel like the initial animation from framer should always happen, even when going back and forth between the routes.

Any help here is greatly appreciated. Thank you so very much!
Answered by Austrian Black and Tan Hound
Ahhh, I found out a kind of janky solution, but it works flawlessly 🤓

"use client";

import { useRouter } from "next/navigation";
import { useState, useEffect, startTransition, useEffectEvent } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Dialog } from "radix-ui";

export default function Sidecart() {
  const [isOpen, setIsOpen] = useState(false);
  const router = useRouter();

  useEffect(() => {
    setIsOpen(true);
  }, []);

  const onAnimationComplete = useEffectEvent(() => {
    if (!isOpen) router.back();
  });

  return (
    <Dialog.Root
      open={isOpen}
      onOpenChange={(open) => startTransition(() => setIsOpen(open))}
    >
      <AnimatePresence>
        {isOpen && (
          <motion.span
            variants={{
              initial: {},
              enter: {},
              exit: {},
            }}
            initial="initial"
            animate="enter"
            exit="exit"
            className="absolute h-[1px]"
            onAnimationComplete={onAnimationComplete}
          >
            <Dialog.Portal forceMount>
              <Dialog.Overlay asChild>
                <motion.div
                  variants={{
                    initial: { opacity: 0 },
                    enter: { opacity: 1 },
                    exit: { opacity: 0 },
                  }}
                  className="fixed inset-0 bg-neutral-900/30"
                />
              </Dialog.Overlay>
              <Dialog.Content asChild forceMount>
                <motion.div
                  variants={{
                    initial: { scale: 0.5, y: "10%", opacity: 0 },
                    enter: { scale: 1, y: 0, opacity: 1 },
                    exit: { scale: 0.5, y: 0, opacity: 0 },
                  }}
                  className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-default-surface p-[25px] shadow-[var(--shadow-6)] focus:outline-none"
                >
                  <Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
                    Edit profile
                  </Dialog.Title>
                  <Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
                    Make changes to your profile here. Click save when you're
                    done.
                  </Dialog.Description>
                  <div className="mt-[25px] flex justify-end">
                    <Dialog.Close asChild>
                      <button className="inline-flex h-[35px] items-center justify-center rounded bg-green4 px-[15px] font-medium leading-none text-green11 outline-none outline-offset-1 hover:bg-green5 focus-visible:outline-2 focus-visible:outline-green6 select-none">
                        Save changes
                      </button>
                    </Dialog.Close>
                  </div>
                  <Dialog.Close asChild>
                    <button
                      className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 bg-gray3 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
                      aria-label="Close"
                    ></button>
                  </Dialog.Close>
                </motion.div>
              </Dialog.Content>
            </Dialog.Portal>
          </motion.span>
        )}
      </AnimatePresence>
    </Dialog.Root>
  );
}
View full answer

9 Replies

Austrian Black and Tan HoundOP
Ahhh, I found out a kind of janky solution, but it works flawlessly 🤓

"use client";

import { useRouter } from "next/navigation";
import { useState, useEffect, startTransition, useEffectEvent } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Dialog } from "radix-ui";

export default function Sidecart() {
  const [isOpen, setIsOpen] = useState(false);
  const router = useRouter();

  useEffect(() => {
    setIsOpen(true);
  }, []);

  const onAnimationComplete = useEffectEvent(() => {
    if (!isOpen) router.back();
  });

  return (
    <Dialog.Root
      open={isOpen}
      onOpenChange={(open) => startTransition(() => setIsOpen(open))}
    >
      <AnimatePresence>
        {isOpen && (
          <motion.span
            variants={{
              initial: {},
              enter: {},
              exit: {},
            }}
            initial="initial"
            animate="enter"
            exit="exit"
            className="absolute h-[1px]"
            onAnimationComplete={onAnimationComplete}
          >
            <Dialog.Portal forceMount>
              <Dialog.Overlay asChild>
                <motion.div
                  variants={{
                    initial: { opacity: 0 },
                    enter: { opacity: 1 },
                    exit: { opacity: 0 },
                  }}
                  className="fixed inset-0 bg-neutral-900/30"
                />
              </Dialog.Overlay>
              <Dialog.Content asChild forceMount>
                <motion.div
                  variants={{
                    initial: { scale: 0.5, y: "10%", opacity: 0 },
                    enter: { scale: 1, y: 0, opacity: 1 },
                    exit: { scale: 0.5, y: 0, opacity: 0 },
                  }}
                  className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-default-surface p-[25px] shadow-[var(--shadow-6)] focus:outline-none"
                >
                  <Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
                    Edit profile
                  </Dialog.Title>
                  <Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
                    Make changes to your profile here. Click save when you're
                    done.
                  </Dialog.Description>
                  <div className="mt-[25px] flex justify-end">
                    <Dialog.Close asChild>
                      <button className="inline-flex h-[35px] items-center justify-center rounded bg-green4 px-[15px] font-medium leading-none text-green11 outline-none outline-offset-1 hover:bg-green5 focus-visible:outline-2 focus-visible:outline-green6 select-none">
                        Save changes
                      </button>
                    </Dialog.Close>
                  </div>
                  <Dialog.Close asChild>
                    <button
                      className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 bg-gray3 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
                      aria-label="Close"
                    ></button>
                  </Dialog.Close>
                </motion.div>
              </Dialog.Content>
            </Dialog.Portal>
          </motion.span>
        )}
      </AnimatePresence>
    </Dialog.Root>
  );
}
Answer
@Austrian Black and Tan Hound Ahhh, I found out a kind of janky solution, but it works flawlessly 🤓 ts "use client"; import { useRouter } from "next/navigation"; import { useState, useEffect, startTransition, useEffectEvent } from "react"; import { AnimatePresence, motion } from "motion/react"; import { Dialog } from "radix-ui"; export default function Sidecart() { const [isOpen, setIsOpen] = useState(false); const router = useRouter(); useEffect(() => { setIsOpen(true); }, []); const onAnimationComplete = useEffectEvent(() => { if (!isOpen) router.back(); }); return ( <Dialog.Root open={isOpen} onOpenChange={(open) => startTransition(() => setIsOpen(open))} > <AnimatePresence> {isOpen && ( <motion.span variants={{ initial: {}, enter: {}, exit: {}, }} initial="initial" animate="enter" exit="exit" className="absolute h-[1px]" onAnimationComplete={onAnimationComplete} > <Dialog.Portal forceMount> <Dialog.Overlay asChild> <motion.div variants={{ initial: { opacity: 0 }, enter: { opacity: 1 }, exit: { opacity: 0 }, }} className="fixed inset-0 bg-neutral-900/30" /> </Dialog.Overlay> <Dialog.Content asChild forceMount> <motion.div variants={{ initial: { scale: 0.5, y: "10%", opacity: 0 }, enter: { scale: 1, y: 0, opacity: 1 }, exit: { scale: 0.5, y: 0, opacity: 0 }, }} className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-default-surface p-[25px] shadow-[var(--shadow-6)] focus:outline-none" > <Dialog.Title className="m-0 text-[17px] font-medium text-mauve12"> Edit profile </Dialog.Title> <Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11"> Make changes to your profile here. Click save when you're done. </Dialog.Description> <div className="mt-[25px] flex justify-end"> <Dialog.Close asChild> <button className="inline-flex h-[35px] items-center justify-center rounded bg-green4 px-[15px] font-medium leading-none text-green11 outline-none outline-offset-1 hover:bg-green5 focus-visible:outline-2 focus-visible:outline-green6 select-none"> Save changes </button> </Dialog.Close> </div> <Dialog.Close asChild> <button className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 bg-gray3 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none" aria-label="Close" ></button> </Dialog.Close> </motion.div> </Dialog.Content> </Dialog.Portal> </motion.span> )} </AnimatePresence> </Dialog.Root> ); }
Siberian
I'm also coming across this issue and it's so annoying.
Austrian Black and Tan HoundOP
Yeah @Siberian , I think this is because they're using the new Activity api here which means the component is pseudo mounted, it's just display: none. The above solution also doesn't work if someone just presses the back button to go to the previous route.

It's definitely weird and my found solution is definitely a hack. The ViewTransition api definitely works better, but it doesn't allow for nearly enough animation customization 🙁 .
Siberian
Yup. That's what I thought too. I'm surprised barely anybody is mentioning this. I liked how it worked on the original PPR (v15) but seems like for now I have to skip out on cacheComponents. I don't know of any workarounds so it is an unfortunate trade off.
Looks like someone's mentioned it already on Framer Motion's repository
Austrian Black and Tan HoundOP
The way I found which works on both manual closing and through browser back button is completely unmount the component via:
export default function Component() {
  const path = usePathname();
  const isPath = path === "/you/path/here"
  
  const [isOpen, setIsOpen] = useState(false);
  const router = useRouter();

  useEffect(() => {
    setIsOpen(true);
  }, []);
  return isPath && (
    <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
       {/* Dialog stuff here */}
    </Dialog.Root>
  )
}
@Siberian Looks like someone's mentioned it already on Framer Motion's repository
Austrian Black and Tan HoundOP
Yeah I saw that xD. Looks like they have the api already built out for[ early access](https://motion.dev/docs/react-animate-activity).
Siberian
Oh wow I didn't see that in EA 👀