Suppose you set window.history.scrollRestoration = "manual" on a webpage.
In Chrome/Firefox
Whenever you click an anchor, the page scrolls to the linked element's position, and whenever you go back/forward through history, the scroll position remains the same but the fragment part of the url is updated (#sectionXYZ).
In Safari
Whenever you click an anchor nothing happens, and whenever you navigate back/forward through history, the page scrolls to the position of the element linked to the current page fragment (#sectionXYZ).
When I say "navigate through history" I mean by either using window.history.back(), window.history.forward() and window.history.go(N) or by using the browser's back/forward buttons.
In the example below you have 2 buttons (blue and red) which will push 2 different state onto the history stack when clicked.
Try to click them several times and navigate back/forward through history to replicate the behaviors I described.
Why is Safari restoring the scroll position of the page even when history.scrollRestoration is set to manual ? Is there a way to prevent this behavior like Chrome and Firefox do ?
html,body,div {
height: 100vh;
width: 100vw;
margin: 0;
overflow-x: hidden;
}
#nav {
display:flex;
justify-content:center;
position:fixed;
width: 100%;
height: 100px;
background: rgba(0,0,0,0.7);
}
nav > a {
display:grid;
justify-content:center;
align-items:center;
text-decoration:none;
color:white;
width: 30%;
height: 90%;
font-size:120%;
cursor:pointer;
}
#a1, #blue{
background-color:blue;
}
#a2, #red {
background-color:red;
}
<body>
<nav id = "nav">
<a id = "a1" href = "#blue">BLUE</a>
<a id = "a2" href = "#red">RED</a>
</nav>
<div id = "blue"></div>
<div id = "red"></div>
<script>
window.history.scrollRestoration = "manual";
window.addEventListener("popstate", () => {
console.log("blue: ", document.getElementById("blue").getBoundingClientRect());
console.log("red: ", document.getElementById("red").getBoundingClientRect());
});
document.getElementById("a1").addEventListener("click", () => window.history.pushState("blue", "", "#blue"));
document.getElementById("a2").addEventListener("click", () => window.history.pushState("red", "", "#red"));
</script>
</body>
I (think I) finally have all the answers!
TL;DR
Safari only jumps to position when you call
history.pushStateand the 3rd parameter (the url) contains a valid hash for the current page.To fix that just pass to
history.pushStatea modified hash (as the 3rd parameter) that you will parse in thepopstateevent listener.For instance:
What is this happening?
According to the HTML5 spec, whenever a history entry that contains a valid fragment is pushed onto the history's stack, the page:
My understanding of this bit is that every browser should always scroll to a fragment if its history entry is valid and that Safari doesn't follow the spec whenever
scrollRestorationis set to"manual"and this is why the provided example won't scroll the the#redor#bluesections.Other than (supposedly) not following the spec, Safari had a different popstate/scrollRestoration behavior than Chrome and Firefox.
Infact, my first question was:
In this case the HTML5 spec states that:
Since Safari never scrolled to
#redor#bluesections it should have never saved any persisted scroll position of any restorable scrollable region (or at least they shouldn't be the correct/updated ones) while I assume that Chrome and Firefox saved them whenever an anchor was clicked.When navigating through the provided example's history Chrome and Firefox never restored any scrollPosition and that is exaclty what the
scrollRestorationspec says. Safari on the other hand seems to never save the persisted scroll positions and so it can't possibly restore any.I suppose that
scrollRestoration="manual"on Chrome/Firefox means "save the scroll positions but do not restore them whenever the user traverse the history", whilst on Safari it seems to mean "do not save any scroll position" (which implies it can't restore any).There's a problem though: Safari scrolls to the correct fragment whenever the user traverse the history even when
scrollRestoration="manual". This led me to think that safari internally always checks the current fragment, if it's a valid one it scrolls to its position and then fires thepopstateevent (which tecnically isn't breaking the spec but its making thescrollRestorationparameter completly useless).What's a possible solution?
My second question was:
Since Safari breaks the
scrollRestorationfeature for anchors, we can't rely on that (it's still useful for Chrome/Firefox though).Before I supposed that Safari internally always checks the scroll position of the current fragment and if it's valid it scrolls to it: good news this seems to actually be the case. Infact a hacky yet functional solution I found was to just "pass the wrong" fragment inside the URL whenever we call
pushState.e.g.
This solves the fact that Safari always restores the scroll position at the cost of having to add 1 line of URL parsing inside the "onpopstate" eventListener.
This is the fixed original example:
Side note
If you're trying to achieve a custom anchor (smooth) scrolling you may be interested in the API I wrote for smooth scrolling and the
uss.hrefSetupfunction: https://github.com/CristianDavideConte/universalSmoothScroll