Next.js Discord

Discord Forum

Page-Transition with Framer-Motion routing problem

Unanswered
Giant Angora posted this in #help-forum
Open in Discord
Giant AngoraOP
Hi folks!

I was trying to implement a page transition following this tutorial (https://blog.olivierlarose.com/articles/nextjs-page-transition-guide - which is in style of Dennis Snellenberg's page transition) but in NextJS with TypeScript and App Router and it's giving me a LOT of headaches for almost a week now. I'm trying to be as specific as I can describing the problem and the different approaches. I took.

First, my page transition consists of two different animations - an easy fade-out-fade-in of the pages themselves and the SVG Curve animation of the tutorial. It took a while to get the exit animations working, as the use of a template.tsx file suggested for handling such animations within Next wasn't really working at all. So I started with the following client.tsx component which does the trick.

layout.tsx:
<html lang={currentLocale} suppressHydrationWarning>
      <body className={cn('antialiased overflow-x-clip', myFont.className)}>
        <Header />
        <main>
          <Suspense fallback={null}>
            <NavigationEvents />
          </Suspense>
          <Client>{children}</Client>
        </main>
      </body>
    </html>

5 Replies

Giant AngoraOP
client.tsx:
'use client';

import Curve from '@/components/Curve';
import Scroller from '@/components/Scroller';
import { AnimatePresence, motion } from 'framer-motion';
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { usePathname } from 'next/navigation';
import { PropsWithChildren, useContext, useRef } from 'react';

// Prevents instant page opening
function FrozenRouter(props: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext ?? {});
  const frozen = useRef(context).current;

  return <LayoutRouterContext.Provider value={frozen}>{props.children}</LayoutRouterContext.Provider>;
}

// Client wraps any client/rsc components with AnimatePresence
export default function Client({ children }: PropsWithChildren) {
  const pathname = usePathname();

  return (
    <AnimatePresence initial={false} mode='wait'>
      <Curve>
        <motion.div
          key={pathname}
          initial={{ y: 20, opacity: 0 }}
          animate={{ y: 0, opacity: 1, transition: { delay: 0.5, duration: 1, ease: [0.76, 0, 0.24, 1] } }}
          exit={{ y: -20, opacity: 0, transition: { duration: 0.35, ease: [0.76, 0, 0.24, 1] } }}
        >
          <FrozenRouter>
            <Scroller>{children}</Scroller>
          </FrozenRouter>
        </motion.div>
      </Curve>
    </AnimatePresence>
  );
}


Notice that the <FrozenRouter> component was crucial to have the exit animations working.

So the main difference to Olivier's approach is to not wrap each page in a Curve component, but putting it here. When I tried to put Curve in each page I had the phenomenon that the animation was fading in and out with the pages themselves instead of laying above and transition should be done in the background.
My Curve.tsx looks like this:

'use client';

import { AnimatePresence, motion } from 'framer-motion';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useStore } from '../lib/store';

interface CurveProps {
  children: React.ReactNode;
}

interface SVGProps {
  height: number;
  width: number;
  backgroundColor: string;
}

const anim = (variants: any) => {
  ...
};

const text = {
  ...
};

const curve = (initialPath: any, targetPath: any) => {
  ...
};

const translate = {
  ...
};

const SVG = ({ height, width, backgroundColor }: SVGProps) => {
  ...
};

export default function Curve({ children }: CurveProps) {
  const pathname = usePathname();
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  const { color, name } = useStore((state) => state.routeData);

  useEffect(() => {
    function resize() {
      setDimensions({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    resize();
    window.addEventListener('resize', resize);
    return () => {
      window.removeEventListener('resize', resize);
    };
  }, [pathname]);

  console.log(color, name);

  return (
    <>
      <AnimatePresence initial={false} mode='wait'>
        <div key={pathname} className='min-h-screen curve'>
          {color}
          <div style={{ opacity: dimensions.width == null ? 1 : 0, backgroundColor: color }} className='background pointer-events-none' />
          <motion.p
            className='absolute left-1/2 top-1/2 text-5xl -translate-x-1/2 -translate-y-1/2 text-center text-contrast pointer-events-none z-50'
            style={{ color: color }}
            {...anim(text)}
          >
            {name}
            {color}
          </motion.p>
          <div style={{ color: color }}>{dimensions.width != null && <SVG {...dimensions} backgroundColor={color} />}</div>
          {children}
        </div>
      </AnimatePresence>
    </>
  );
}
Now to the overall problem and what I want to achieve. The background color of the SVG Curve is black by default. I have some dynamic project pages inside my app, which can specify a different color for each project that I can grab with all the data from my api. In case I am navigating to one of those pages I want the background color to be set to that specific color. This kinda works, but the problem is that no matter what I do the color as well as the name of route I am navigating to are set halfway through the animation - means when the routes are changing in the background or the exit animation of the old route is finished. The color and the name should be provided instantly. Since I have tried so many things to grab name and color beforehand (useState, use LocalStorage etc.) When I look at the LocalStorage in developer tools, it is getting the color instantly when I click on a route, but my Curve component is only updating on route change instead of immediately. I think the problem lies in the overall construct, but I cannot grab my head around a solution.
I can try to attach a short clip of the problem.
Giant AngoraOP
Any ideas?