next.js
next.js copied to clipboard
Opt out of scroll restoration when using browser back buttons
Describe the feature you'd like to request
There is a clear way to opt out of Next.js scroll restoration when using Link
or router.push
: just pass { scroll: false }
. However, I haven't found a clear way to opt out of scroll restoration when using the browser back/forward buttons.
I spent a little while reading the source of the popState
handler, and it seems like the router will always jump either to the scroll position in sessionStorage or to the top of the page.
Describe the solution you'd like
Ideally, there would be some kind of router configuration option by which an app could opt out of the router messing with scroll position completely.
Describe alternatives you've considered
I've considered trying to mess with the scroll value stored in sessionStorage
during onPopState
, but this seems like a really convoluted workaround that might not hold up in future Next.js releases.
I’ve found a pretty good solution in the following:
router.beforePopState((state) => {
state.options.scroll = false;
return true;
});
Next.js should not reset the scroll position to 0,0 on back or forwards navigation. Could you please share a reproducible demo for this behavior? We have tests covering this in Chrome.
Sure, @Timer:
I created a repository showing the behavior by the following steps:
- created a new app with
create-next-app
- I duplicated the example page to create two pages, A and B, with a link between them, and duplicated some of the content to let the pages scroll
- I added a useEffect hook to set
window.history.scrollRestoration = 'manual'
, to ensure that the browser is not messing with scroll position at all.
For debugging purposes, I also went into the local node_modules
and made the following modification inside this block of next/client/index.tsx, though that change is not captured in the repository. This message indicates when it is Next.js initiating the scroll up to the top of the page, rather than the browser or another piece of code.
if (input.scroll) {
+ console.log('Scrolling to', input.scroll.x, input.scroll.y);
window.scrollTo(input.scroll.x,input.scroll.y);
}
Now, I can navigate from page A to page B, and observe that when I press the browser “back” button, a message is printed to the console indicating that Next.js is scrolling the page to the top. Here’s a video:
https://user-images.githubusercontent.com/10377391/104229558-6d072600-541a-11eb-90b6-0258b6d98e8e.mov
You can see that the console message is printed, indicating that Next.js initiates a scroll to (0, 0).
The behavior you just displayed in video was correct, though.
The scroll should be restored to its location on Page A (the top). I agree the console log is weird, but what happens if you scroll Page A before going to B, and then back to A?
I can't reproduce this issue in a normal app (console.log aside, the scroll doesn't appear to be taking affect even though it's called).
Here's the alternate test case you described. I:
- scroll down on page A
- move to page B
- press “back” to page A
As you can see, the page still jumps to the top of the page, and the scrollTo code in client/index.jsx still executes with (0, 0). Here's a video showing that:
https://user-images.githubusercontent.com/10377391/104344406-1c9bd100-54cb-11eb-87b6-8b4378bd3347.mp4
I'm not sure that even the first behavior I showed was correct, though — given that I set history.scrollRestoration = 'manual'
and your comment that “Next.js should not reset the scroll position to 0,0 on back or forwards navigation,” I wouldn't expect that the scroll position would change at all when clicking the back button. What am I missing?
Oh, this seems to be specific to window.history.scrollRestoration = 'manual'
being configured as manual
and not auto
.
I can fix this.
We are having a problem with this behavior too. @Timer, do you have any planned date when this issue is expected to be fixed? It would help us a lot.
This issue blocking me too. There is no way to disable this unnecessary behavior for now.
I can fix this.
I've tried this but it didn't work.
Hi @Timer
Can we use
module.exports = {
experimental: {
scrollRestoration: true
}
}
as a workaround while this issue is not officially solved ?
Any updates on this? Also trying to implement custom scroll restoration (via Router.on('routeChangeComplete')
), and noticing that on clicking the browser back button, after scrolling down a bit on a page, that it pops it back to the top before my custom restoration happens on the incoming page.
To clarify I have the following in place, per official docs and various recommendations:
-
useEffect
that setswindow.history.scrollRestoration = 'manual'
- Next Links have
scroll={false}
per the docs
This only happens on browser back/forward buttons.
FYI this worked for me:
import Head from "next/head";
export default function ScrollRestorationDisabler() {
return (
<Head>
{/* Tell the browser to never restore the scroll position on load */}
<script
dangerouslySetInnerHTML={{
__html: `history.scrollRestoration = "manual"`,
}}
/>
</Head>
);
}
FYI this worked for me:
import Head from "next/head"; export default function ScrollRestorationDisabler() { return ( <Head> {/* Tell the browser to never restore the scroll position on load */} <script dangerouslySetInnerHTML={{ __html: `history.scrollRestoration = "manual"`, }} /> </Head> ); }
It works with me using the scroll
prop in the Link component.
https://nextjs.org/docs/api-reference/next/link#disable-scrolling-to-the-top-of-the-page
As I understand the scrollRestoration issue persists.
I have checked that when I set experimental: { scrollRestoration: true }
on every page change my console gives me:
> window.history.scrollRestoration
> 'manual'
We need the scroll to be restored every time we navigate with the browser. If history.scrollRestauration
is set to auto
the browser should take care of it, but in my case the browser takes care of it before the previous page is reloaded
=> means my view jumps and then the previous page is loaded at scroll (0,0).
If I use the experimental feature it should be handled by next.JS somewhere window.history.scrollRestoration = manual
, but the result is unfortunately that the scroll position is not restored in production and I end up at (0,0). At least the page is not jumping prematurely before the previous one is loaded.
Oh, this seems to be specific to
window.history.scrollRestoration = 'manual'
being configured asmanual
and notauto
.I can fix this.
I am seeing the same behavior as @controversial regardless of whether I have window.history.scrollRestoration
set to manual or auto. Scroll down, navigate away, click back button, get returned to top of page instead of where I was.
Workarounds from @controversial, @lipoolock, @petrogad have no effect either. Note this is "next": "^11.0.1",
. Any chance I just need 12 for one of the workarounds to take effect?
Hi, I also recently ran into this bug. I was trying to figure out what was causing it and in doing so I found a somewhat sketchy but effective workaround.
In my case, I was implementing my own scrolling tracker so I could use framer motion with next. The only time I want something to use window.scrollTo
is in my scrolling handler, so I added the following code to it:
useEffect(() => {
const { scrollTo } = window;
window.scrollTo = ({ left, top, behavior, allowed }) => {
if (allowed) {
scrollTo({ left, top, behavior, allowed });
}
};
return () => (window.scrollTo = scrollTo);
}, []);
And when I call window.scrollTo
, I add the allowed
argument like so:
window.scrollTo({ left: 0, top: position, allowed: true })
This of course will only work if you're implementing your own scroll tracker like I am, but hopefully it helps people as a temporary workaround. Hopefully the fix comes through soon.
To override both Next.js and native scrollRestoration, try useScrollRestoration
by @claus:
- https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
A combination of both beforePopState
and window.history.scrollRestoration = 'manual'
does the trick for me:
export const Transition: React.FC = ({ children }) => {
const router = useRouter();
useEffect(() => {
router.beforePopState(state => {
state.options.scroll = false;
return true;
});
}, []);
return (
<Script>{`window.history.scrollRestoration = "manual"`}</Script>
...
);
};
No scroll at all when navigation back or forth.
To override both Next.js and native scrollRestoration, try
useScrollRestoration
by @claus:
- https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
This works for me!
To override both Next.js and native scrollRestoration, try useScrollRestoration by @claus: https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
This worked well for me as well!
Hi @Timer
Can we use
module.exports = { experimental: { scrollRestoration: true } }
as a workaround while this issue is not officially solved ?
Respect
Why am I still facing this issue? Is this haven't fixed yet? Currently I am using Next 12.1.5
Worked fine for me:
You can enable it in your next.config.js file this way:
module.exports = { experimental: { scrollRestoration: true, }, };
There is an issue with all of these solutions that I am not sure has been noticed. On iOS WebKit there is a check that a snapshot should only be shown for a back or forward action if shouldRestoreScrollPosition || (currentScrollPosition == snapshot->viewScrollPosition())
, shouldRestoreScrollPosition
is equivalent to history.scrollRestoration = "auto"
. Setting history.scrollRestoration = "manual"
means the current page must be scrolled to the same position as the page being transitioned to (forward or back) in order for the UI to look "normal" when navigating.
Here is an example on the Target website that uses Next:
https://user-images.githubusercontent.com/608869/187505346-fe887b8b-df52-49e1-8018-7647a2d6c170.mov
Notice if both screens are scrolled to top the snapshot is shown, but Target has some form of scroll restoration it seems so the manual setting screws up the page.
The only solution I found so far for my own project is to check for webkit and not set scrollRestoration
to manual
in that case, as window.history.scrollRestoration = 'auto'
doesn't work for my project anyway this seems to give the desired result.
if (navigator.userAgent.indexOf('AppleWebKit') != -1) {
window.history.scrollRestoration = 'auto';
} else {
window.history.scrollRestoration = 'manual';
}
@nhannah Thanks for noticing that iOS safari issue! Hopefully the Next authors will take it into account if they ever address this glaring problem. Gatsby has excellent built-in scroll restoration so we've been quite surprised to have to patch it. Users have been posting about it since 2017 🤷
Your sniff is a little too broad though, gets picked up by Firefox. I used,
// Manual doesn't work well on iOS Safari https://github.com/vercel/next.js/issues/20951#issuecomment-1231966865
const ua = window.navigator.userAgent.toLowerCase();
const isMobileSafari = /safari/.test(ua) && /iphone|ipod|ipad/.test(ua);
window.history.scrollRestoration = isMobileSafari ? 'auto' : 'manual';
I updated the hook to include that and some other niceties at the main gist here: https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81?permalink_comment_id=4301903#gistcomment-4301903
@tomiviljanen CC @ijjk hi I tried the experimental scroll restoration and it worked pretty well.
It has a few outstanding issues though:
- scroll doesn't restore on page refresh
- when going back, if your page has a number of react components, restore happens too fast and it will end up jumping to the bottom of the page (I added a 1ms timeout workaround in my version of the community hook)
-
scroll-behavior: smooth
doesn't work well in Next. You jump to the wrong scroll location on new pages a la this post https://github.com/vercel/next.js/issues/3303#issuecomment-371269290 and when going back, instead of the bottom of the page I get an incorrect position that's slightly higher on the page.
I know there are a number of other open issues out there on all of this but just wanted to list these out clearly. Seems that the experimental feature isn't super close to production-ready yet.
When testing the feature, I'd suggest using a full web page with a good amount of content on it, since it seems like React's hydration lifecycle interacts with this and there are little race conditions happening. (I don't mean this as a dig at all, but you might also peek at how Gatsby does theirs as it is quite solid.)
A combination of both
beforePopState
andwindow.history.scrollRestoration = 'manual'
does the trick for me:export const Transition: React.FC = ({ children }) => { const router = useRouter(); useEffect(() => { router.beforePopState(state => { state.options.scroll = false; return true; }); }, []); return ( <Script>{`window.history.scrollRestoration = "manual"`}</Script> ... ); };
No scroll at all when navigation back or forth.
Working nicely for me
For me it was just the window history scroll restoration
window.history.scrollRestoration = "manual";
Here is a good article I found https://developer.chrome.com/blog/history-api-scroll-restoration/
Dropping a note that experimental.scrollRestoration
not working with react-query
- had to opt for community hook
Describe the feature you'd like to request
There is a clear way to opt out of Next.js scroll restoration when using
Link
orrouter.push
: just pass{ scroll: false }
. However, I haven't found a clear way to opt out of scroll restoration when using the browser back/forward buttons.I spent a little while reading the source of the
popState
handler, and it seems like the router will always jump either to the scroll position in sessionStorage or to the top of the page.Describe the solution you'd like
Ideally, there would be some kind of router configuration option by which an app could opt out of the router messing with scroll position completely.
Describe alternatives you've considered
I've considered trying to mess with the scroll value stored in
sessionStorage
duringonPopState
, but this seems like a really convoluted workaround that might not hold up in future Next.js releases.
Just add this in nextjsconfig Tested and working 👍
module.exports = { experimental: { scrollRestoration: true, }, };