Intersection Observer comes to Firefox

What do infinite scrolling, lazy loading, and online advertisements all have in common?

They need to know about—and react to—the visibility of elements on a page!

Unfortunately, knowing whether or not an element is visible has traditionally been difficult on the Web. Most solutions listen for scroll and resize events, then use DOM APIs like getBoundingClientRect() to manually calculate where elements are relative to the viewport. This usually works, but it’s inefficient and doesn’t take into account other ways in which an element’s visibility can change, such as a large image finally loading higher up on the page, which pushes everything else downward.

Things get worse for advertisements, since real money is involved. As Malte Ubl explained in his presentation at JSConf Iceland, advertisers don’t want to pay for ads that never get displayed. To make sure they know when ads are visible, they cover them in dozens of tiny, single-pixel Flash movies whose visibility can be inferred from their framerate. On platforms without Flash, like smartphones, advertisers set up timers to force browsers to recalculate the position of each ad every few milliseconds.

These techniques kill performance, drain batteries, and would be completely unnecessary if the browser could just notify us whenever an element’s visibility changed.

That’s what IntersectionObserver does.

Hello, new IntersectionObserver()

At its most basic, the IntersectionObserver API looks something like:

let observer = new IntersectionObserver(handler);
observer.observe(target); // <-- Element to watch

The demo below shows a simple handler in action.

A single observer can watch many target elements simultaneously; just repeat the call to observer.observe() for each target.

Intersection? I thought this was about visibility?

By default, IntersectionObservers calculate how much of a target element overlaps (or “intersects with”) the visible portion of the page, also known as the browser’s “viewport:”

Illustration of a target element partially intersecting with a browser's viewport

However, observers can also monitor how much of an element intersects with an arbitrary parent element, regardless of actual on-screen visibility. This can be useful for widgets that load content on demand, like an infinitely scrolling list inside a container div. In those cases, the widget could use IntersectionObservers to help load just enough content to fill its container.

For simplicity, the rest of this article will discuss things in terms of “visibility,” but remember that IntersectionObservers aren’t necessarily limited to literal visibility.

Handler basics

Observer handlers are callbacks that receive two arguments:

  1. A list of IntersectionObserverEntry objects, each containing metadata about how a target’s intersection has changed since the last invocation of the handler.
  2. A reference to the observer itself.

Observers default to monitoring the browser’s viewport, which means the demo above just needs to look at the isIntersecting property to determine if any part of a target element is visible.

By default, handlers only run at the moment when target elements transition from being completely off-screen to being partially visible, or vice versa, but what if you want to distinguish between partially-visible and fully-visible elements?

Thresholds to the rescue!

Working with Thresholds

In addition to a handler callback, the IntersectionObserver constructor can take an object with several configuration options for the observer. One of these options is threshold, which defines breakpoints for invoking the handler.

let observer = new IntersectionObserver(handler, {
    threshold: 0 // <-- This is the default
});

The default threshold is 0, which invokes the handler whenever a target becomes partially visible or completely invisible. Setting threshold to 1 would fire the handler whenever the target flips between fully visible and partially visible, and setting it to 0.5 would fire when the target passes point of 50% visibility, in either direction.

You can also supply an array of thresholds, as shown by threshold: [0, 1] in the demo below:

Slowly scroll the target in and out of the viewport and observe its behavior.

The target starts fully visible—its intersectionRatio is 1—and changes twice as it scrolls off the screen: once to something like 0.87, and then to 0. As the target scrolls back into view, its intersectionRatio changes to 0.05, then 1. The 0 and 1 make sense, but where did the additional values come from, and what about all of the other numbers between 0 and 1?

Thresholds are defined in terms of transitions: the handler fires whenever the browser notices that a target’s intersectionRatio has grown or shrunk past one of the thresholds. Setting the thresholds to [0, 1] tells the browser “notify me whenever a target crosses the lines of no visibility (0) and full visibility (1),” which effectively defines three states: fully visible, partially visible, and not visible.

The observed value of intersectionRatio varies from test to test because the browser must wait for an idle moment before checking and reporting on intersections; those sorts of calculations happen in the background at a lower priority than things like scrolling or user input.

Try editing the codepen to add or remove thresholds. Watch how it changes when and where the handler runs.

Other options

The IntersectionObserver constructor can take two other options:

  • root: The area to observe (default: the browser viewport).
  • rootMargin: How much to shrink or expand the root’s logical size when calculating intersections (default: "0px 0px 0px 0px").

Changing the root allows an observer to check for intersection with respect to a parent container element, instead of just the browser’s viewport.

Growing the observer’s rootMargin makes it possible to detect when a target nears a given region. For example, an observer could wait to load off-screen images until just before they become visible.

Browser support

IntersectionObserver is available by default in Edge 15, Chrome 51, and Firefox 55, which is due for release next week.

A polyfill is available which works effectively everywhere, albeit without the performance benefits of native implementations.

Additional Resources:

About Dan Callahan

Engineer with Mozilla Developer Relations, former Mozilla Persona developer.

More articles by Dan Callahan…


8 comments

  1. Simon

    Let’s say I have a scrollable container element that contains a large number of image elements, e.g. … (think of a image library). Many of those will be loaded off-screen by the browser. Does FF not already do some optimization (loading and painting only the once visible images) and would Intersection Observer interfere with that?

    August 3rd, 2017 at 00:06

    Reply

  2. Šime Vidas

    Those of you who’d like to try out different options in the demo, switch to the “debug view.” It seems that the `rootMargin` option does not work in the other modes on CodePen.

    August 3rd, 2017 at 01:10

    Reply

  3. Eric Shepherd

    Hey all! I wrote the docs on MDN for this API, so if you’re playing with it and something in the docs is unclear or needs work, and you don’t want to update the docs yourself (don’t forget — MDN is a wiki!), be sure to let us know about the problem by reporting it with this form: https://bugzilla.mozilla.org/form.doc.

    I had a lot of fun documenting this API. It’s really well done, and can be used creatively in so many ways. Enjoy!

    August 4th, 2017 at 07:12

    Reply

  4. Eduardo

    Amazing content. I’m working on a project and now I’ve decided to rewrite the code with that feature. Thank you!

    August 4th, 2017 at 07:35

    Reply

  5. Oliver

    Is it necessary to for of or forEach over entries in the callback handler?
    The below code works (for simple cases), and is a bit simpler.

    function observerCallback(entries, observer) {
      if (entries[0].isIntersecting) {
        document.body.style.backgroundColor = "blue"
      } else {
        document.body.style.backgroundColor = "red"
      }
    }

    August 9th, 2017 at 02:04

    Reply

    1. Dan Callahan

      Looping through the array of entries is important: depending on the other work that the browser’s doing, it’s possible that several events will get queued and then passed to the handler all at once, even in otherwise simple scenarios.

      August 9th, 2017 at 13:00

      Reply

  6. Jeremy Wagner

    I’m implementing this into a lazy loading library. Should I use unobserve or disconnect when all of my elements are lazy loaded, or does the browser handle removing observers automatically?

    August 10th, 2017 at 11:22

    Reply

    1. Dan Callahan

      Yes, you should manually remove the observer when you’re done with it.

      As you mentioned, you have two options:

      1. Use observer.disconnect() to completely turn off the Observer, for all of the targets it’s watching.
      2. Use observer.unobserve(target) to turn off the Observer for a specific target.

      In addition to properties like isIntersecting and intersectionRatio, the IntersectionObserverEntry passed to your handler has a target property that contains a reference to the target, which you can pass to unobserve().

      August 11th, 2017 at 11:47

      Reply

Post Your Comment