Native Infinite Scrolling with the IntersectionObserver API

Giulio Mainardi
Share

This article was peer reviewed by Simon Codrington and Tim Severien. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Recently an interesting new client-side JavaScript API appeared on the Web Platform, the IntersectionObserver API.

This tiny but useful API provides a means to efficiently monitor (observe) the visibility of specified DOM elements, that is, when they are in or out of a viewport (the viewport of the browser window or of an element). The definition of element visibility can be made precise specifying the fraction of the area of the element that intersects the viewport rectangle.

Some common applications and use cases for this feature include:

  • Lazy-loading of content
  • Infinite scrolling
  • Ads visibility
  • Animations triggered by scrolling (note: this is not a target use case. The visibility information reported by the API might come with a slight delay and pixel-perfect data are not guaranteed).

Browser Support

Being a fairly new API, its support at the time of this writing is still limited:

  • Chrome desktop 51
  • Chrome for Android 51
  • Android WebView 51
  • Opera 38
  • Opera for Android 38

However, an in-development polyfill (there is no support for the root margin) is available on Github, so we can start to play with Intersection Observers right now.

In this article, we’ll implement the infinite scrolling UX pattern. We’ll use the aforementioned polyfill and even several ES6/ES2015 features along the way such as promises, template strings, and arrow functions.

Infinite Scrolling

Imagine we have a long list of items that we want to browse with infinite scrolling, so that when the user approaches the bottom of the document the next batch of items are loaded and appended to the end of the list.

Here is what we’ll be building:

See the Pen Infinite Scrolling Demo by SitePoint (@SitePoint) on CodePen.

The core idea that will be developed in the following code snippets, is to use an item near the bottom of the list, and so near the bottom of the document, as a sentinel to signal when the browser viewport is coming near the end of the page.

This sentinel will be the DOM element that will be monitored by an IntersectionObserverinstance. When this object reports the sentinel visibility, we know that it is the time to load the next set of items. Once they are loaded, rendered and appended to the list, we pick a new sentinel for the next page.

Setting up the page

So, let’s start with the HTML. The page body markup is simply a list:

<ul class="listview"></ul>

Normally this list should be already populated with the first items but to simplify the code, these items will be fetched from JavaScript, just like the successive pages loaded on scrolling.

Then we include the polyfill. In a real scenario, we could load it only if necessary. We even check if the API is supported to display a notice on screen:

<span class="polyfill-notice">The polyfill is in use</span>
<script>
  if (!('IntersectionObserver' in window))
    document.body.classList.add('polyfill');
</script>
<script src="../intersectionobserver-polyfill.js"></script>

Regarding the CSS, we use some rules mainly to setup the layout of the list view and to style the support notice . Because this is not the scope of the article, please refer to the stylesheet for details.

Creating the script

We start by instantiating an IntersectionObserver object. We only need one instance because it will be used to monitor all sentinels:

sentinelObserver = new IntersectionObserver(sentinelListener, {threshold: 1})

In the configuration object passed in the second argument, we set a visibility threshold (the fraction of the element area visible) to 1 to fire the event listener (passed as the first argument) only when the item is fully within the viewport. For the purposes of the demo, the sentinel element is identified with an orange border.

The event listener will perform the operations we outlined above. Before we look at its code, let’s introduce an object to represent and manage the current sentinel element:

sentinel = {
  el: null,
  set: function(element) {
    this.el = element;
    this.el.classList.add('sentinel');
    sentinelObserver.observe(this.el);
  },
  unset: function() {
    if (!this.el)
      return;
    sentinelObserver.unobserve(this.el);
    this.el.classList.remove('sentinel');
    this.el = null;
  }
}

The most relevant lines here are where the IntersectionObserver methods observe() and unobserve() are invoked to attach and detach the element to be monitored.

Now the event listener can use this helper object to remove the current sentinel, setup a loading indicator at the bottom of the list and load the next list page with the nextPage method. This loads, renders and appends the new items. It returns a Promise to indicate when these operations are completed. At that time we can pick the new sentinel item and turn off the loading indicator:

sentinelListener = function(entries) {
  console.log(entries);
  sentinel.unset();
  listView.classList.add('loading');
  nextPage().then(() => {
    updateSentinel();
    listView.classList.remove('loading');
  });
}

The updateSentinel method picks the next sentinel, choosing the first item of the new loaded page:

updateSentinel = function() {   sentinel.set(listView.children[listView.children.length - pageSize]);
}

The rest of the code consists mainly in the implementation of the nextPage function. When the promise returned by loadNextPage() (that simulates a network request) is resolved, the provided items objects are rendered in HTML and appended at the end of the list.

And that’s it! Refer back to the demo to see all the code fragments assembled together.

Further Reading

Here are some reference links to more thorough documentation, covering both the API and its rationale:

Are you looking forward to seeing more browsers implementing the IntersectionObserver API soon?