Techcookies

Understand intersection observer API

JavaScript | Thu Sep 19 2024 | 4 min read

JavaScript has 3 observer based APIs, one being Intersection and other two being Resize Observer and Mutation Observer. Intersection Observer in my opinion is the most useful because of how easy it makes things like infinite scrolling, lazing loading images, and scroll based animations. In JavaScript, attaching an event listener on 'scroll' to constantly trigger callback function on scroll can be performance-intensive, and if may end up having a sluggish user experience. Intersection Observer API helps build a better and well performant UI.

Intersection observer API helps detect the visibility of an element, i.e. if it’s in the current viewport, and also the relative visibility of two elements in relationship to each other - triggering a callback function.

Use cases

  • Lazy loading of images.
  • Infinite scrolls.
  • Data pre-fetching.
  • To detect whether an ad was viewed or not.
  • To determine whether a user has read an article.
  • To run costly renderings and animations only when they are visible on the screen.

Basic Implementation

javascript
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #page-bottom {
            background-color: #dbe6eb;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
        }

        .hidden {
            opacity: 0;
            transform: translateX(100%);
        }
    </style>
</head>

<body>
    <div id="page-bottom">
        <h2>Section without image loaded.</h2>
    </div>
        <img class="lazy-load" data-src="image.jpg" alt="A beautiful scene">
        <img class="lazy-load" data-src="image2.jpg" alt="Another beautiful scene">


        <script>
            document.addEventListener('DOMContentLoaded', () => {
                let lazyImages = document.querySelectorAll('.lazy-load');

                const observer = new IntersectionObserver((entries) => {
                    entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            let image = entry.target;
                            image.src = image.getAttribute('data-src');
                            image.onload = () => image.removeAttribute('data-src');
                            observer.unobserve(image);
                        }
                    });
                }, {
                    rootMargin: '0px',
                    threshold: 0.1 // Adjust threshold based on requirement
                });

                const pageBottom = document.querySelector('#page-bottom');
                lazyImages.forEach(image => {
                    observer.observe(image);
                });
            });
        </script>
</body>

</html>
alt text

We can see in the above image, when the page loaded image has not been loaded(can be verified in network tab). As we'll scroll down, root(document) intersect with images(target) and it loads images. This is an example how lazy-loading can be implemented using Intersection observer API.

alt text

The Technical Stuff

To use Intersection Observer, we need to first create a new observer, which takes two parameters: An object with the observer’s options, and the callback function that we want to execute whenever the element we’re observing (known as the observer target) intersects with the root (the scrolling container, which must be an ancestor of the target element).

javascript
const intersectionHandler = (entries) =>{
 entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            //code that depends on intersection
                        }
            });

}

const option =  {
                    root: document.querySelector('root-div'),
                    rootMargin: '0px',
                    threshold: 0.1 // Adjust threshold based on requirement
                }
 const observer = new IntersectionObserver(intersectionHandler, option)

When we’ve created our observer, we need to create a observer to instruct it to watch a target element:

javascript
const targetEl = document.querySelector('target-div')

observer.observe(targetEl)
  1. You can assume root as the outer rectangle, or the rectangle within which you want to observe for an intersection.
  2. The root element can be any parent element or body or document or browser viewport.
  3. If you specify root as null or skip denfining in the option object, the browser viewport will be the root element.
  4. target is the element which you are watching.
  5. threshold gives you the extent to which the overlap should occur.
  6. rootMargin is very similar to CSS margin that applied to the root before the extent of the intersection is calculated.

rootMargin

The rootMargin is like adding CSS margins to the root element. It can take positive and negative values as well.
Using rootMargin as negative value, element can technically be classed as "intersecting" even when it is out of view, refer Diagram 6 for details.

Default value for rootMargin is 0px.

alt text

threshold

The threshold can accept a value between 0 and 1 and represents the percentage of the element that must be visible/within the root bounds for it to be considered intersecting.

By default this is set to 0 which means as soon as any part of the element is visible it will be considering intersecting.

You can also pass an array to threshold which means that the Intersection Observer will fire each time your element passes one of the thresholds passed to it.

javascript
option =  {
        root: document.querySelector('root-div'),
        rootMargin: '0px',
        threshold: [0, 0.25, 0.5, 0.75, 1]
    }
alt text