1. Web Design
  2. Scroll Events

Intersection Observer: Track Elements Scrolling Into View


The “Intersection Observer” provides a way to asynchronously observe changes in the intersection (overlapping) of elements. This can be in relation to other elements, or the viewport. In this article we’ll take a look at a few demos and discuss the relevance that Intersection Observer will play in the future for web developers. I’ll also share code examples to help get you started for those late night experimentation sessions. Let’s dive in!

What Can the Intersection Observer Do?

The IntersectionObserver API lets you register a callback function which is executed whenever an element being monitored enters or exits another element, or the viewport

There are plenty of ideas and suggestions shared in this W3C explainer doc on GitHub, however, Intersection Observer can be handy for natively building out features such as:

  • Lazy Loading
  • Infinite Scrolling
  • Reporting Visibility/Location
  • Executing Animations

Check out this demo by Dan Callahan to clarify what we’re talking about:

To get started with IntersectionObserver let’s explore the appropriate coding steps required. You can always make sure your intended browsers/devices support the IO API by referencing caniuse. We’ll begin by creating an observer and discussing how it can be used to monitor components.

Creating an Observer

 IntersectionObserver starts by requiring a group of options defined as an object literal and passed as an argument to your defined observer object.

These observer options on lines 2-4 will dictate some important details when it comes to detecting the visibility of a target element in relation to the root. The first argument of the observer object (last line) represents a callback (function) that’s executed as requirements are met by your observer. The second argument refers to our object literal containing the observer’s options and accepts the following properties:

  • root: The element you want to test the intersection against. A value of null refers to the browser’s viewport. You can also pass in DOM selector methods such as document.querySelector('#mytargetobject').
  • rootMargin: If you need to expand or shrink the effective size of the root element before computing intersections. These values passed are similar to the CSS margin property. If the root element is specified, the values can be percentages.
  • threshold: Either a single number or an array of numbers indicating what percentage of the target’s visibility triggers the observer’s callback. A value of 1.0 means the threshold isn’t considered passed until every pixel is visible, whereas 0 means the element is out of view completely.

As I previously mentioned, you’ll handle the callback argument by creating a function containing custom logic based on your project’s needs. This logic is executed anytime an observed element(s) is visible in relation to the defined root element.

Logic within your observer’s function can be events such as loading images, adding/removing classes, or controlling visibility, but the choice is yours as to what can be done from within depending on your needs and goals.

Every time the observer detects a change, a changes event is reported (kind of like a function() reporting an events object) from the observer callback. By using this triggered event, we can check for visibility of our element in relation to the root before running any additional logic by detecting properties on the change event. Some developers will replace changes with the word entries instead, but both approaches work the same.

This loop states “For each change detected, check to see if the target element is currently visible (greater than 0) in relation to the root defined.” The intersection ratio assists in reporting how much of the element is visible using a value between 0.0 (not visible) and 1.0 (fully visible). You can think of intersection ratio just like the threshold property defined in your observer’s options.

The Observe Method

So far we’ve created an options object, a callback function and defined an observer object, but we still don’t have anything to observe. This is where the observe method will come in service.

This method adds an element to the set of target elements being watched by the IntersectionObserver. In this example I’m observing every image on the page by the results obtained from the images selector reference. The observe() method will continue monitoring a target until any of the following occurs: the unobserve() or disconnect() methods are called along with the target element or intersection root deleted. If you no longer need to observe an element it’s best to make a call to unobserve() and pass in your target as the argument.

Support Condition

If you need to feature test the support of this API you can wrap everything in an if/else conditional statement like so:

Since the Intersection Observer API still resides at a working draft stage I encourage you to feature detect the window object, or you can also find polyfills if you prefer.

Live Examples

The following demo contains all the code I’ve discussed thus far with some additional tweaks. This demo lazy loads images when they’re displaying 50% of their visibility in relation to the viewport.

You might notice a differing behavior when it comes to the picture elements compared to the img elements in Chrome and Firefox. Both browsers load picture elements perfectly, but picture disregards our threshold even with absolute size constraints defined. They appear to load when they’re about 10% in view vs. the 50% threshold defined in the demo. Leave a comment below if you notice this oddity, or have experienced issues with IntersectionObserver and picture specifically.

Here’s another demo creating an infinite scrolling scenario that lazy loads imagery using Ajax to load imagery as needed.

In this scenario, I’m inserting a sentinel to the DOM as my observed target. This sentinel is placed next to the last item within the infinite scroller, and as the sentinel comes into view, the callback loads the data, creates the next item(s), attaches them to the DOM and repositions the sentinel. If you properly recycle the sentinel, no additional calls to observe() are required. Here’s a great diagram by the Google Developer team to help visually explain this approach.

A Note on Imagery

When an img element with a constraint of max-width: 100% is observed, it will fail to load. That means any imagery loaded in a lazy fashion must have constraints defined in the CSS or inline accordingly The problem with picture elements lacking any constraints is that there’s zero size before their content is loaded; meaning that they all intersect the viewport simultaneously as you scroll. I suspect getBoundingClientRect() is part of the reason as this is one of the primary methods to obtain an element’s coordinates and constraints.


Are you using IntersectionObserver in your own work right this minute? Are you excited for this feature and the possibilities it brings? Could IntersectionObserver replace the need for event-based features such as position: sticky? Personally I feel this new API is a solid addition to our specifications and I eagerly look forward to its continued growth over the coming years. 

I’ve included some useful links below and encourage you to dive deeper in your free time. Each link will help you gain a better understanding of how this new API works and functions. If you have any tips or tricks for other readers leave them in the comments below. As always, happy coding!


Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.