Image for post
Image for post
Delayed scroll restoration in Firefox using our polyfill.

Maintaining scroll positions in all browsers

A few months ago I was riding the subway taking positions on the Brigade web app. I noticed something strange. Whenever I didn’t have a strong opinion I checked out the reasons people had left to inform myself. Then I would use the back button to take positions again, only to have to scroll down all the way to where I was before. That was a bad experience—I had to do a lot of scrolling—especially since I like to take a lot of positions!

Now, I had done the same thing in Chrome on my MacBook before, and I never noticed this problem. When I got home, I verified this, and indeed, it worked fine on my laptop—it scrolled back to my last taken position. Then I tried out other browsers on my laptop, such as Firefox and Safari, and aha! Those browsers had the same problem as my iPhone. I found it so annoying that I started investigating.

It turns out that all browsers restore the scroll position when clicking the back button, right after the page has loaded. However, with single page applications like ours, a page load typically doesn’t mean all content on the page is available; we first need to make some AJAX requests before we can render things. So for traditional websites most browsers work fine, but for newer web apps navigating around can suck—except in Chrome.

Existing solutions

It turns out that this is such a debated topic in the react-router community, that they even split out a separate repo for different scroll behaviours.

Looking at how other people solve this problem, I found that our navigation library at the time, pjax, had functionality for caching page content when navigating away, so that pages can be immediately restored when navigating back. However, it assumed having static content, and didn’t seem to play well with React, which we use for our rendering, as React components would lose their state.

We were considering switching to react-router (which we have now done), so I looked if they had a better solution. It turns out that this is such a debated topic in the react-router community, that they even split out a separate repo for different scroll behaviours. Unfortunately, none of them supported waiting for the content to be rendered, like Chrome did.

Writing our own polyfill

The next question is, where to scroll to? Our application uses the History API for all page navigation, so I decided not to worry about full page loads. It turns out that you can store information whenever calling pushState or replaceState, so I modified both functions to store the scroll position whenever calling them. Combining that with restoring the scroll position when popState is called, and it already comes pretty close to Chrome’s behaviour!

Interestingly, people have argued that this is a bug in the spec, and in fact Chrome has deliberately implemented this differently.

Those modifications worked well for navigating to new pages, but not so much for pressing “back” and then “forward” again, because we didn’t save the scroll position from before clicking the “back” button. So I wanted to also store the scroll position when popState was fired, because that means that the browser navigates to another place. However, it turns out that’s not so easy, because the spec states that the scroll position should be restored before firing popState, so we would not be able to determine the actual scroll position. Interestingly, people have argued that this is a bug in the spec, and in fact Chrome has deliberately implemented this differently. So I had to do without scroll restoration when pressing the “forward” button.

Another deviation from Chrome’s behaviour, is that it doesn’t attempt to restore the scroll position if the user has already scrolled. Unfortunately, in Javascript-land there is no way for us to tell if a scroll was initiated by the user, or by the browser’s scroll restoration algorithm (which, as we just learned, is even inconsistent across browsers, so it’s hard to make assumptions about it). Luckily, both this problem and the popState problem will likely be fixed by the Custom Scroll Restoration Proposal, a new API that allows Javascript developers to disable the browser’s scroll behaviour altogether. It has already been shipped in—you guessed it—Google Chrome. Hopefully other browser vendors will follow suit soon.

Anyway, we’ve had this polyfill enabled on Brigade’s production web app for a few months now without any problems, so we decided to make it open source. If you have similar scrolling issues with your web app, just install the delayed-scroll-restoration-polyfill. And, as always, we welcome all feedback!

Written by

Materialistic minimalist. Optimistic realist. Creative copycat. Rationalises believing in paradoxes.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store