Native Infinite Scrolling with the IntersectionObserver API

Share this article

Clay figure holding a magnifying glass

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?

Frequently Asked Questions about Intersection Observer API

What is the Intersection Observer API and how does it work?

The Intersection Observer API is a powerful tool in the web development toolkit. It provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport. This API is particularly useful for implementing lazy loading of images, infinite scrolling, and other similar features. It works by creating an observer instance with a callback function, which is invoked whenever the visibility of the target element changes. The callback function receives a list of IntersectionObserverEntry objects, each representing an element being observed.

How do I use the Intersection Observer API?

To use the Intersection Observer API, you first need to create an instance of the IntersectionObserver object. This object takes two parameters: a callback function and an options object. The callback function is invoked whenever the target element intersects with either the viewport or a specified element. The options object allows you to customize the conditions under which the callback function is invoked. Once the observer is created, you can start observing an element by calling the observe() method on the observer instance and passing the target element as a parameter.

What are the options I can pass to the Intersection Observer API?

The Intersection Observer API accepts an options object that allows you to customize its behavior. This object can have the following properties: root, rootMargin, and threshold. The root property specifies the element that is used as the viewport for checking visibility of the target. The rootMargin property is similar to the CSS margin property and allows you to specify a margin for the root. The threshold property is either a single number or an array of numbers indicating at what percentage of the target’s visibility the observer’s callback should be executed.

Can I observe multiple elements with the same observer?

Yes, you can observe multiple elements with the same Intersection Observer. To do this, you simply call the observe() method on the observer instance for each element you want to observe. The callback function will then be invoked for each of these elements whenever they meet the visibility conditions specified in the options object.

How do I stop observing an element?

To stop observing an element, you call the unobserve() method on the observer instance, passing the target element as a parameter. This will stop the observer from receiving notifications of the target element’s intersection with the root.

What is the IntersectionObserverEntry object?

The IntersectionObserverEntry object is passed to the callback function whenever the target element intersects with the root. This object contains information about the intersection, including the time of intersection, the target element, the root bounds, the bounding rectangle of the target element, and the intersection ratio.

What is the intersection ratio?

The intersection ratio is a property of the IntersectionObserverEntry object. It is a measure of how much of the target element is visible within the root. It is a number between 0.0 and 1.0, where 0.0 means no part of the target element is visible, and 1.0 means the entire target element is visible.

Can I use the Intersection Observer API in all browsers?

The Intersection Observer API is widely supported in all modern browsers. However, for older browsers that do not support this API, you can use a polyfill to provide the same functionality.

What is a polyfill in the context of the Intersection Observer API?

A polyfill is a piece of code that provides the functionality of the Intersection Observer API in browsers that do not support it natively. It mimics the behavior of the API, allowing you to use it in older browsers.

How can I use the Intersection Observer API for lazy loading of images?

The Intersection Observer API is perfect for implementing lazy loading of images. You can create an observer that watches for when an image element enters the viewport. When this happens, the callback function can replace the src attribute of the image element with the actual image URL, causing the image to be loaded.

Giulio MainardiGiulio Mainardi
View Author

Website

apiinfinite scrollingIntersectionObserver
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week