Track Element Visibility Using Intersection Observer

Abstract image used to represent Track Element Visibility Using Intersection Observer
Image by Denys Rodionenko.

In Brief

IntersectionObserver reports geometric intersection with a root, often the viewport, without requiring our own scroll polling. It sends an initial observation and later reports relevant changes. Positive root margins can trigger work before an element reaches the screen; intersection alone does not prove that the element is unobscured or actually seen.

Historically, tracking an element's position within a user's viewport has been a complicated and arduous task of listening to scroll and resize events and then calculating an element's position using Element.getBoundingClientRect(). However, in the realm of modern web development, the Intersection Observer API stands out as a powerful tool for managing element visibility within the viewport, without the need for intensive inbrowser event listeners and calculations.


What is Intersection Observer?

Intersection Observer lets us asynchronously observe geometric overlap between a target and a root, such as an ancestor or the viewport. That suits deferred loading and infinite scrolling. For analytics or advertising, ordinary intersection is only part of a visibility test: another element could cover the target, for example.

I use it here on my personal website in a few different places, perhaps the most obvious being the clap buttons at the bottom of each article and portfolio project. Because I don't want my visitors to have to connect to the claps endpoint immediately on pageload, I use Intersection Observer to detect when a visitor is scrolling near to the button, and ony then trigger the API call as it gets close to coming into view.


How to Use Intersection Observer

A Basic Example

interface VisibilityCallback {
  (entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
}

const callback: VisibilityCallback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Element has entered the viewport');
    }
  });
};

const observer = new IntersectionObserver(callback);
const targetElement = document.getElementById('target');
if (targetElement) {
  observer.observe(targetElement);
}

This TypeScript example logs when entry.isIntersecting is true. Calling observe() also produces an initial entry, so an element already intersecting when observation begins can trigger the message without first moving into view.

A More Detailed Example

The code above is all very well and good if you just want to trigger an event every time an element is scrolled into view. However, Intersection Observer offers a lot more configurability than that, and makes it much more useful for realworld uses where for example you might only want to trigger the event once and then stop listening, or might want to trigger it when it is a certain percentage either within or outside the viewport.

So, in this example, we'll configure the Intersection Observer to observe an element with specific margins and thresholds. We'll also set it up to unobserve the target after the first intersection.

interface VisibilityCallback {
  (entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
}

const advancedCallback: VisibilityCallback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Element intersects the expanded root');
      // This is where you trigger any functionality you want for when the
      // element intersects

      // Stop observing after the first intersection
      observer.unobserve(entry.target);
    }
  });
};

// Configuration options for the Intersection Observer
const options = {
  rootMargin: '10px 20px 30px 40px',  // top, right, bottom, left margins around the root
  threshold: [0, 0.5, 1],  // 0%, 50%, 100% intersection ratios
};

const advancedObserver = new IntersectionObserver(advancedCallback, options);
const targetElement = document.getElementById('advanced-target');
if (targetElement) {
  advancedObserver.observe(targetElement);
}

The Code Explained

This is a much more nuanced use of Intersection Observer which showcases some of the features you might want or expect when tracking element visibility:

  1. Callback function:
    advancedCallback checks entry.isIntersecting against the root expanded by the margins. It performs the work and calls observer.unobserve(entry.target). With these positive margins, that work can happen before the target reaches the visible viewport.
  2. Options Object
    : I've defined an options object that specifies:
    1. rootMargin: the four positive values expand the viewport boundary at the top, right, bottom and left. This can be useful for starting a request shortly before the target comes into view.
    2. threshold: the ratios 0, 0.5 and 1 describe the target's intersecting area relative to its boundingbox area. They do not measure visible pixels or account for an overlay covering the target.
  3. Observer Creation
    : An IntersectionObserver is created with the advancedCallback and the options. It's then used to observe targetElement.
  4. Once and stop:
    this callback stops observing at the first intersection. It therefore does not wait for the 0.5 or 1 threshold if an earlier intersection has already triggered the work.

Replacing Traditional Methods

As I touched upon at the start of this article, historically developers would have to rely on event listeners for scroll and resize events to determine whether an element was visible in the viewport or not. However, this approach can lead to performance issues:

  • Scroll Event Issues

    : Continuously handling scroll events can result in janky animations and unresponsive scrolling, especially if the event handler includes heavy computations or DOM manipulations.
  • Debouncing and throttling can reduce scrollhandler work, but the handler still needs to decide what to measure. An observer can remove that manual polling when geometric intersection is the condition we need.

Performance Benefits of Intersection Observer

  • The browser tracks intersections, so our code can avoid repeated scrollposition calculations.
  • Observation queues an initial entry. Later entries report threshold crossings or changes in intersection state; callbacks may receive several entries together.
  • Performance still depends on the work we do in the callback and the number of targets. Keep that work small and measure the result rather than assuming the API makes every implementation faster.

The Wrap‑up

Intersection Observer is a useful way to trigger work when an element approaches or intersects a chosen region. Choose the margins and thresholds for that task, keep the callback light, and use a separate definition of visibility if you need to know whether someone could actually see the content.


Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.