Intersection Observer

Determines the intersection of an element with its parent or the browser window.

Time to read: 9 min

Briefly

Intersection Observer — a browser API that allows you to asynchronously track the intersection of an element with its parent or the document's viewport. When an intersection occurs, an action can be triggered, such as loading additional posts in the news feed ("infinite scroll") or performing "lazy" content loading.

Example

For clarity, the observation area is highlighted with a yellow dashed border, and below is shown how images scroll into this area. It can be seen that images start to load when they intersect the dashed line, that is, slightly earlier than they appear in the visible area. This is possible thanks to the rootMargin property.

Another feature — Morty's image slightly enlarges when it is fully within the observed area. This trick is done using the threshold and intersectionRatio properties, which are explained below.

Open demo in the new window

The simplified code for this example looks approximately like this:

        
          
          const lazyImages = document.querySelectorAll('.lazy-image')const callback = (entries, observer) => {  entries.forEach((entry) => {    if (entry.isIntersecting) {      console.log('The user has almost scrolled to the image!')      entry.target.src = entry.target.dataset.src      observer.unobserve(entry.target)    }  })}const options = {  // root: default is window,  // but you can specify any container element  rootMargin: '0px 0px 75px 0px',  threshold: 0,}const observer = new IntersectionObserver(callback, options)lazyImages.forEach((image) => observer.observe(image))
          const lazyImages = document.querySelectorAll('.lazy-image')

const callback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('The user has almost scrolled to the image!')

      entry.target.src = entry.target.dataset.src
      observer.unobserve(entry.target)
    }
  })
}

const options = {
  // root: default is window,
  // but you can specify any container element
  rootMargin: '0px 0px 75px 0px',
  threshold: 0,
}

const observer = new IntersectionObserver(callback, options)

lazyImages.forEach((image) => observer.observe(image))

        
        
          
        
      

How it works

Intersection Observer is created using the constructor:

        
          
          const observer = new IntersectionObserver(callback, options)
          const observer = new IntersectionObserver(callback, options)

        
        
          
        
      

It takes a callback function that will be executed when the intersection occurs and additional intersection settings options.

Callback function

The callback takes two arguments:

1️⃣ entries — a list of objects with information about the intersection. For each observed element, one IntersectionObserverEntry object is created.

The object contains several properties. The most useful ones:

  • isIntersecting — a boolean value. true if there is an intersection between the element and the observed area.
  • intersectionRatio — the proportion of intersection from 0 to 1. If the element is fully in the observed area, the value will be 1, and if halfway — 0.5.
  • target — the observed element itself for further manipulation. For example, for adding classes.

There are other properties that allow you to find out the time and coordinates of the intersection, as well as the sizes and positions of the observed elements.

2️⃣ observer — a reference to the observer instance to call listening methods:

  • observe(element) — starts observing the passed element;
  • unobserve(element) — removes the element from the observed list;
  • disconnect() — stops observing all elements.

options

An optional argument in the form of an object with three properties:

root — the element that will be the observation area. It must be a parent of the observed element. Default is window.

rootMargin — a string with margins for the observation area. The syntax is almost the same as that of the CSS property margin'0px 0px 0px 0px' (top, right, bottom, left), a shorthand is also available, for example, '50px 100px' or '200px'. Default is '0px 0px 0px 0px'. If they are set, the intersection will take these margins into account.

Visualization of the work of rootMargin

threshold — the intersection threshold at which the callback will be triggered. It can be either a single number from 0 to 1, or an array of values, for example, [0, 0.5, 1]. Default is 0.

More about the `threshold` values
- `0` — it will trigger even with intersecting by one pixel; - `1` — it will trigger only when the element is fully appearing in the observed area; - `[0, 0.5, 1]` — the callback will be triggered three times: first when at least one pixel of the element enters the observation area, then when half of the element is in the area, and once more when the element is fully in the observation area.

Understanding

Intersection Observer is named so because it implements the programming pattern of “Observer”. The observer monitors the position of observed elements and performs actions upon their intersection with the container. This works similarly to event subscriptions through the addEventListener() method.

Intersection Observer works asynchronously and does not block the main thread. This allows applications to remain smooth while performing magic, like on Apple's website.

Intersection Observer operates through callbacks. When creating it, you need to describe which elements you want to track and which container's intersection you are monitoring. The callback will then be called each time an intersection occurs between the observed area and any of the tracked elements.

In this code, we check for the intersection of the element and the observation area. Since we do not know which element from our list crossed the container's boundary, we need to find it by iterating over entries. If the elements intersect, a message is logged to the console, and then observing this element is stopped using the unobserve() method. This behavior is suitable, for example, for "lazy" loading.

        
          
          const callback = (entries, observer) => {  entries.forEach((entry) => {    if (entry.isIntersecting) {      console.log('The element crossed the boundary of the area and is still touching it!')      observer.unobserve(entry.target)    }  })}
          const callback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('The element crossed the boundary of the area and is still touching it!')

      observer.unobserve(entry.target)
    }
  })
}

        
        
          
        
      

Another case — it is necessary to determine which part of the element is within the observation area. To do this, we use the options object:

        
          
          const options = {  root: document.querySelector('.container'),  rootMargin: '0px',  threshold: [0, 0.5, 1],}
          const options = {
  root: document.querySelector('.container'),
  rootMargin: '0px',
  threshold: [0, 0.5, 1],
}

        
        
          
        
      

As the observed area, we will take the element with the class container without any additional margins. The interest here is in the threshold property. It means that the callback should be called in three cases:

  • first, the element crossed the area by at least 1 pixel;
  • then half of the element is in the area;
  • and finally, the element is fully inside the observation area.

We need to find out which specific event occurred. For this, the IntersectionObserverEntry has the intersectionRatio property.

        
          
          const callback = (entries) => {  entries.forEach(({ isIntersecting, intersectionRatio }) => {    if (isIntersecting) {      if (intersectionRatio >= 0 && intersectionRatio < 0.45) {        console.log('The element has appeared in the observation area')      }      if (intersectionRatio >= 0.45 && intersectionRatio < 0.75) {        console.log('The element is halfway in the observation area')      }      if (intersectionRatio === 1) {        console.log('The element is fully in the observation area')      }    }  })}
          const callback = (entries) => {
  entries.forEach(({ isIntersecting, intersectionRatio }) => {
    if (isIntersecting) {
      if (intersectionRatio >= 0 && intersectionRatio < 0.45) {
        console.log('The element has appeared in the observation area')
      }

      if (intersectionRatio >= 0.45 && intersectionRatio < 0.75) {
        console.log('The element is halfway in the observation area')
      }

      if (intersectionRatio === 1) {
        console.log('The element is fully in the observation area')
      }
    }
  })
}

        
        
          
        
      

After defining the callback and settings, we create the observer itself and start listening on the element with the class element:

        
          
          const targetElement = document.querySelector('.element')const observer = new IntersectionObserver(callback, options)observer.observe(targetElement)
          const targetElement = document.querySelector('.element')
const observer = new IntersectionObserver(callback, options)

observer.observe(targetElement)

        
        
          
        
      

If necessary, you can stop observing all elements or a specific one:

        
          
          observer.disconnect()observer.unobserve(targetElement)
          observer.disconnect()

observer.unobserve(targetElement)

        
        
          
        
      

Applications

With this tool, you can create infinite feeds, like in many popular social networks, trigger beautiful animations, like on the Apple website, and even create unusual pages, like bertani.net.

Historically, detecting the visibility of a single element or the visibility of two elements in relation to each other has been a challenging task. Solutions were unreliable and slowed down the browser due to operating in the main thread. The Intersection Observer API works asynchronously, improving application performance.

Visual comparisons can be found in the article Scroll listener vs Intersection Observers: a performance comparison.