@@ -18,6 +18,15 @@ import { usePathname } from 'next/navigation'
1818let lastPopstateAt = Number . NEGATIVE_INFINITY
1919const POPSTATE_WINDOW_MS = 200
2020
21+ /**
22+ * Tracks whether any `ScrollToTop` instance has run its mount effect yet.
23+ * Module-scoped so cross-section navigation (which mounts a fresh instance)
24+ * still scrolls — only the very first mount on page load is treated as the
25+ * initial render, letting the browser's native scroll restoration win on
26+ * reload.
27+ */
28+ let hasMounted = false
29+
2130if ( typeof window !== 'undefined' ) {
2231 window . addEventListener ( 'popstate' , ( ) => {
2332 lastPopstateAt = performance . now ( )
@@ -30,14 +39,19 @@ if (typeof window !== 'undefined') {
3039 * Next.js's default scroll handling only brings the new Page element into view,
3140 * which often resolves to "no scroll" inside shared layouts (see vercel/next.js#64435).
3241 *
33- * Skipped when the pathname change closely follows a popstate (preserving browser
34- * back/forward scroll restoration) or when a hash anchor is targeted (letting the
35- * browser's native anchor scroll win).
42+ * Skipped on the initial mount (so browser scroll restoration on reload wins),
43+ * when the pathname change closely follows a popstate (preserving browser
44+ * back/forward scroll restoration), and when a hash anchor is targeted (letting
45+ * the browser's native anchor scroll win).
3646 */
3747export function ScrollToTop ( ) {
3848 const pathname = usePathname ( )
3949
4050 useEffect ( ( ) => {
51+ if ( ! hasMounted ) {
52+ hasMounted = true
53+ return
54+ }
4155 if ( performance . now ( ) - lastPopstateAt < POPSTATE_WINDOW_MS ) {
4256 lastPopstateAt = Number . NEGATIVE_INFINITY
4357 return
0 commit comments