diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index e7c3f02..7d9e862 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -2,7 +2,7 @@ import { motion, AnimatePresence, MotionConfig } from 'framer-motion'; import { usePathname } from 'next/navigation'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; interface PageTransitionProps { children: ReactNode; @@ -31,6 +31,23 @@ interface PageTransitionProps { export default function PageTransition({ children }: PageTransitionProps) { const pathname = usePathname(); + // Reset scroll on route change. Next.js does this automatically, but + // AnimatePresence mode="popLayout" keeps the outgoing page mounted + // during the crossfade, which races the built-in scroll restoration + // and leaves the new page scrolled to wherever the old one was. Hash + // navigations are passed through so in-page anchors still work. + // + // `behavior: 'instant'` is required: globals.css sets + // `scroll-behavior: smooth` on for in-page anchor scrolling, + // and the default form would inherit that — turning the scroll-to-top + // into an animated tween that races the page-transition layout shift + // and lands the user partway up the new page instead of at 0. + useEffect(() => { + if (typeof window === 'undefined') return; + if (window.location.hash) return; + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); + }, [pathname]); + return (