score:7

Accepted answer

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.


Related Query

More Query from same tag