- 5 min read

Follow elements visibility with Intersection Observer

Javascript
Markdown logo

Intersection Observer API, in Javascript, allows to follow the visibility of an element within a page or a container.

Problem

When we scroll through a page, we scroll its content (generally, vertically).

On the right of our screen, a scrollbar indicates our progress in the page.

In a web page, if we wanted to follow the visibility of some elements, we could listen to the scroll event to react to the page scrolling :

window.addEventListener("scroll", () => {
  const scrollPosition = window.scrollY;

  // perform actions based on the position,
  // on what is visible
});

This approach can work, but what is the problem ?

  • The event listener will run continuously, when scrolling, which could overload the main thread
  • To figure out if an element of the page is visible, we will retrieve its position (with getBoundingClientRect() for example) compare it to the scroll position in the page, so we have to manually calculate it
  • If we want to determine the visibility of an element within a scrollable container in the page, then we also have to manually calculate it, based on the position of the parent element

Intersection Observer

Intersection Observer API naturally handles these problems.

Starting from a root element, it will observe elements within a defined intersection zone.

When any of the observed elements enters or exits the intersection zone, then we can execute a callback.

Root element

When creating an instance of IntersectionObserver, we can pass it options.

Among these options, we can specify a root element : root. This element, if not specified, will be the entire page. Otherwise, it can be any scrollable container in which we want to monitor the visibility of some elements.

const observer = new IntersectionObserver(
  callback, // callback function
  {
    // root element
    root: document.getElementById("container"),
  },
);

Callback function

When creating an instance of IntersectionObserver, we register a callback function.

This function will be executed in two situations :

  • When the instance is created
  • When an observed element is considered as entering or exiting the intersection zone

It takes two parameters : elements entering or exiting the intersection zone at the moment, and the actual instance of IntersectionObserver :

const observer = new IntersectionObserver(
  (entries, observer) => {},
  // options...
);

Inside entries, we have an array of elements, each of type IntersectionObserverEntry. We can then loop through them and read useful information, such as :

  • isIntersecting, the most convenient : is the element in the intersection zone ?
  • intersectionRatio, can be useful : what percentage of the element is crossing with the root element ?

Observing elements

It’s very simple to observe an element. Once the instance of IntersectionObserver is created, we call its observe method by passing the element to observe :

observer.observe(element);

Then, as soon as the element crosses the intersection zone, the callback function is triggered.

Intersection zone

By default, the intersection zone covers the entire root element. As soon as an observed element touches this zone, it will be considered as visible.

const observer = new IntersectionObserver(callback);
Bloc 1non visible
Bloc 2non visible
Bloc 3non visible
Bloc 4non visible
Bloc 5non visible
Bloc 6non visible

In the example above, as soon as the elements enter the zone, they are considered as visible. So it feels like all elements are always visible.

rootMargin

It is possible to make the intersection zone bigger or smaller by passing an option to the instance of IntersectionObserver : rootMargin.

The rootMargin option accepts the same syntax as the CSS margin property : top right bottom left. We can therefore indicate one to four values, in pixels, percentage, or both.

Let’s look at an example with rootMargin to -100px, which reduces the intersection zone by 100 pixels on all sides :

const observer = new IntersectionObserver(callback, {
  rootMargin: "-100px",
});
INTERSECTION
Bloc 1non visible
Bloc 2non visible
Bloc 3non visible
Bloc 4non visible
Bloc 5non visible
Bloc 6non visible

Try it yourself with the slider below. We want to reduce the intersection zone, so we will apply a negative value :

-90px
INTERSECTION
Bloc 1non visible
Bloc 2non visible
Bloc 3non visible
Bloc 4non visible
Bloc 5non visible
Bloc 6non visible

threshold

The threshold option allows to add a threshold value, from which the element will be considered as visible. The value can vary :

  • A number between 0 and 1 is equivalent to a percentage between 0% and 100%. For example if we pass 0.8, then the element is considered visible when at least 80% of its content is visible

Example with 0.8 :

const observer = new IntersectionObserver(callback, {
  threshold: 0.8,
});
Bloc 1non visible
Bloc 2non visible
Bloc 3non visible
Bloc 4non visible
Bloc 5non visible
Bloc 6non visible
  • An array of values between 0 and 1, indicating to the instance of IntersectionObserver at what percentage of the target’s visibility the observer’s callback should be executed

Example with a call every 10%, which displays the visibility percentage of the element (intersectionRatio) :

const observer = new IntersectionObserver(callback, {
  threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
});
Bloc 1non visible
Bloc 2non visible
Bloc 3non visible
Bloc 4non visible
Bloc 5non visible
Bloc 6non visible
Output :

Handling a table of contents

An example of Intersection Observer API usage is handling a table of contents.

As we read the article, the current section is highlighted.

This is exactly what Astro Starlight does : when a section title enters the intersection zone, the corresponding table of contents element is highlighted.

Astro Starlight contains a web component. This component contains the creation of the IntersectionObserver instance.

We will also find the intersection zone calculation with the rootMargin option : we take the height of the navbar, we add the height of the table of contents on mobile, and we have the beginning of the intersection zone. 53 pixels later, this is the end of the zone. When a title crosses this zone, the corresponding table of contents element will change its appearance.

Astro Starlight actually defines two web components : one for mobile (displayed at the top of the screen), and one for larger screens (displayed on the right).