/Links-in-Single-Page-Apps?will=they#scroll

A quick look at the state of hash links, broken scrolling in single page apps, and how we fixed it at Scrivito.

The legacy

In the world of JavaScript-based single page apps (SPAs), a lot of things that used to take place on the server, now happen in the client, i.e. the browser on the user’s phone, laptop or computer.

Interestingly, one thing that actually used to be handled by the browser with the traditional website approach, no longer happens in most cases: Scrolling the page to a location identified by the hash of a URL.

With traditional websites, adding a hash mark and fragment identifier to a URL results in scrolling to the corresponding part of the given page. Take as an example the “#syntax-references” part of w3.org/TR/html52/infrastructure.html#syntax-references. If you follow this link, the browser automatically scrolls to the “References” section. The same happens if you follow a link with a hash on the same page or a hash link to a different page on the same site.

The caveat

Modern SPAs no longer follow the concept of a “different page”, that’s where they got their name from. They handle links by listening to clicks and changing the app state accordingly. This logic, in turn, disables parts of the browser’s built-in behavior, including scrolling a referenced element into the user’s view.

To make things even more interesting, dynamic websites populate their content asynchronously, with different bits of content popping up as the data arrives. Even the most recent browsers are not able to keep the URL hash, the position of the page fragment, and the scroll position in sync. Scrivito apps, as an example of modern dynamic SPAs, face this situation as well.

Solutions

The issue has been bugging us and some of our customers for quite some time. Upon further investigation we found no other viable fix and fragmented discussions on how to properly resolve. We’ve seen different solutions popping up here and there, ranging from custom handlers, that could do one thing well, to using 3rd-party libs that did another.

In the end, none of these solutions covered all of the typical use cases at once:

  • Visiting the app with a hash URL should scroll to the referenced element as the page is being loaded
  • Clicking a hash link on the same URL should scroll to the respective element
  • Following a hash link to another URL on the same site should also work as expected

The list becomes a bit longer if you start adding edge cases, for example the expected behavior when using the browser’s back and forward buttons. Or, what happens if you click the same in-page link again? Also, we wanted to support customized logic, like soft scrolling, and the possibility to reference plain text, something we use on scrivito.com, #like in this example.

After looking at the existing libraries, we realized that all of them have the same issue: They are good at what they do, but none of them does all of what we needed. For example,

  • react-hash-link uses a good approach to handling dynamically added content: observing DOM mutations. It knows about a lot of cases, but not all of ours. Looking at the code, we realized that the core logic was simple enough to roll our own instead of trying to make it fit.
  • react-scroll is very versatile, but relies on React components, increasing the complexity of the application. This library was used in the Scrivito Example App, but didn’t work with HTML content stored in the CMS.

So we picked up some inspiration from them and built our own go-to solution:

Hello scroll-to-fragment!

Quoting from the description:

This helper provides single page apps with the classic scrolling behavior. It updates the scroll position on load, and checks for updates on clicks and browser history changes. To keep the fragment in line with asynchronously updated content (for example in a ReactJS-based app) it also adjusts the scroll position on DOM changes.

The usage is as straightforward as:

import { scrollToFragment } from "scroll-to-fragment";

scrollToFragment();

To keep track of programmatic URL changes, pass in the history object of your app:

import { createBrowserHistory } from "history";
const myBrowserHistory = createBrowserHistory();

scrollToFragment({ history: myBrowserHistory });

We will continuously update this library to meet the typical scroll-to expectations with SPAs.

We tried to build the library in a framework-agnostic fashion. Since we are focused on React and Scrivito, we expect that users of different frameworks and development environments will be confronted with different coverage of their use cases, and we would like to hear about this.

If you miss something, or found another edge case (of which there are many, judging from our own findings), feel free to submit an idea. Or even better, send us a pull request!

And finally, we hope that one day this library will be superseded by modern standards and better default browser behavior. But as long as we’re not there yetーenjoy!

What do you think about this article?

Learn more about enterprise SaaS CMS Scrivito

More great blog posts from David Császár