How to Navigate and Scroll to an Element with ID in a Next.js Page wrapped with AnimatePresence

15,750

The Culprit is the exitBeforeEnter prop on on AnimatePresence. Removing the prop fixes the hash id navigation but breaks some of my use-case.

If set to true, AnimatePresence will only render one component at a time. The exiting component will finish its exit animation before the entering component is rendered. - framer-motion docs

I couldn't just remove the exitBeforeEnter prop as I had included it to fix a bug I had where targeting a node in the entering page collided with the identical one in the old instance of the exiting page. For example a ref logic on an animated svg header in the exiting Page colliding with the entering page's header svg ref logic.

To get the best of both worlds, Using the onExitComplete that "Fires when all exiting nodes have completed animating out", I passed it a callback that checks for the hash from the widow.location.hash and smooth scrolls to the id using scrollIntoView Note: onExitComplete is only effective if exitBeforeEnter prop is true.

// pages/_app.tsx
import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { AnimatePresence } from 'framer-motion';

// The handler to smoothly scroll the element into view
const handExitComplete = (): void => {
  if (typeof window !== 'undefined') {
    // Get the hash from the url
    const hashId = window.location.hash;

    if (hashId) {
      // Use the hash to find the first element with that id
      const element = document.querySelector(hashId);

      if (element) {
        // Smooth scroll to that elment
        element.scrollIntoView({
          behavior: 'smooth',
          block: 'start',
          inline: 'nearest',
        });
      }
    }
  }
};

const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
  const router = useRouter();
  return (
    <AnimatePresence exitBeforeEnter onExitComplete={handExitComplete}>
      <Component {...pageProps} key={router.route} />
    </AnimatePresence>
  );
};

export default MyApp;


Live CodeSandbox here.

PS: For some reason the the window.location.hash in the sandbox preview is always an empty string, breaking the hash navigation but opening the preview in a separate browser tab works like a charm.

Share:
15,750
iamcastelli
Author by

iamcastelli

Apparently, this user prefers to keep an air of anonymity.

Updated on June 15, 2022

Comments

  • iamcastelli
    iamcastelli almost 2 years

    I am using Framer Motion to animate Next.js page transitions. However using the using AnimatePresence breaks the hash link navigation and the page no longer goes to the targeted id element.

    The page transitions are perfect until you want to navigate to a harsh ID on the page :(

    // I have a link component setup like this
    // index.tsx
    <Link href="/about#the-team" scroll={false}>
      <a>The Team</a>
    </Link>
    
    // Targeting another page `about.tsx` with the id
    // about.tsx
    
    {/* ...many sections before.. */}
    <section id="the-team">{content}</section>
    

    I have a custom _app.tsx as shown below.

    // _app.tsx
    import { AppProps } from 'next/app';
    import { useRouter } from 'next/router';
    import { AnimatePresence } from 'framer-motion';
    
    const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
      const router = useRouter();
      return (
        <AnimatePresence exitBeforeEnter>
          <Component {...pageProps} key={router.route} />
        </AnimatePresence>
      );
    };
    
    export default MyApp;
    
    

    I am expecting to go directly to the section with id="the-team" but it won't work. A refresh of the page with the hash link shows that it's originally at the target element but quickly jumps to the top. It's so fast and easy to miss. How do I retain the page transitions but still be able to navigate to hash id?

  • Eric Crescioni
    Eric Crescioni over 3 years
    Thank you so much for this, had the exact problem on an infinite scroll catalog (stored in a Context); this is exactly what I needed.
  • iamcastelli
    iamcastelli over 3 years
    Glad it was helpful
  • Franco
    Franco about 3 years
    Great!, a question and how to I add it to the same page, did not work for me. codesandbox.io/s/…
  • Ray Caballero
    Ray Caballero about 2 years
    Thank you for this! I added a short delay before it scrolls into the element since I have enter/exit animations. :)