Track Element Visibility Using Intersection Observer

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 in‑browser 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 page‑load, 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 real‑world 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:
Callback function:
advancedCallbackchecksentry.isIntersectingagainst the root expanded by the margins. It performs the work and callsobserver.unobserve(entry.target). With these positive margins, that work can happen before the target reaches the visible viewport.Options Object
: I've defined anoptionsobject that specifies: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.threshold: the ratios0,0.5and1describe the target's intersecting area relative to its bounding‑box area. They do not measure visible pixels or account for an overlay covering the target.
Observer Creation
: AnIntersectionObserveris created with theadvancedCallbackand theoptions. It's then used to observetargetElement.Once and stop:
this callback stops observing at the first intersection. It therefore does not wait for the0.5or1threshold 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 scroll‑handler 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 scroll‑position 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.