Skip to content

Instantly share code, notes, and snippets.

@hatsumatsu
Last active January 19, 2025 14:54
Show Gist options
  • Save hatsumatsu/faac1f0d3dedb2a4d8e158e8512c162c to your computer and use it in GitHub Desktop.
Save hatsumatsu/faac1f0d3dedb2a4d8e158e8512c162c to your computer and use it in GitHub Desktop.
PageTransition.jsx
import { useRouter } from 'next/router';
import { useState, useEffect, useRef } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
/**
* Manual scroll restoration
*
*/
function useScrollRestoration(mainRef) {
const router = useRouter();
const scrollTop = useRef([0, 0]);
const isBrowserHistory = useRef(false);
// disable automatic scroll restoration
useEffect(() => {
if (window.history.scrollRestoration) {
window.history.scrollRestoration = 'manual';
}
}, []);
useEffect(() => {
// set flag when navigating using the browser history
function onPopState(state) {
state.options.scroll = false;
isBrowserHistory.current = true;
setTimeout(() => {
isBrowserHistory.current = false;
}, 2000);
return true;
}
// save current scrollTop before navigating away
function onRouteChangeStart() {
scrollTop.current[1] = scrollTop.current[0];
scrollTop.current[0] = document.scrollingElement.scrollTop;
}
router.beforePopState(onPopState);
router.events.on('routeChangeStart', onRouteChangeStart);
return () => {
router.events.off('routeChangeStart', onRouteChangeStart);
};
}, [router]);
// set scroll position after exit animation
// check for browser history flag -> scroll to saved position
// check for hash -> scroll to element
// default: scroll to 0
function onExitComplete() {
/**
* We have to add an arbitrary timeout to make sure
* the new <main> element is mounted and the page
* is tall enough to jump to the previous position
* 50ms seems fine
*/
setTimeout(() => {
// scroll to hash > ID
let targetElement;
const hash = router.asPath.split('#')?.[1];
if (hash) {
targetElement = document.querySelector(`#${hash}`);
}
const newScrollTop = isBrowserHistory.current
? scrollTop.current[1]
: targetElement
? targetElement.offsetTop
: 0;
document.scrollingElement.scrollTo({
top: newScrollTop,
behavior: 'instant',
});
}, 50);
}
return { onExitComplete };
}
function PageTransitions({ children }) {
const mainRef = useRef();
const router = useRouter();
const { onExitComplete } = useScrollRestoration(mainRef);
return (
<AnimatePresence initial={false} mode="wait" onExitComplete={onExitComplete}>
<motion.main
initial={{
opacity: 0,
// ... transition
}}
animate={{
opacity: 1,
// ... transition
}}
exit={{
opacity: 0,
// ... transition
}}
key={router.asPath.split('#')?.[0]}
ref={mainRef}
>
{children}
</motion.main>
</AnimatePresence>
);
}
export { PageTransitions };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment