This is an experimental technology
Because this technology's specification has not stabilized, check the compatibility table for usage in various browsers. Also note that the syntax and behavior of an experimental technology is subject to change in future versions of browsers as the specification changes.

This page is not complete.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

Historically, detecting visibility of an element, or the relative visibility of two elements in relation to each other, has been a difficult task for which solutions have been unreliable and prone to causing the browser and the sites the user is accessing to become sluggish. Unfortunately, as the web has matured, the need for this kind of information has grown. Intersection information is needed for many reasons, such as:

  • Lazy-loading of images or other content as a page is scrolled.
  • Implementing "infinite scrolling" web sites, where more and more content is loaded and rendered as you scroll, so that the user doesn't have to flip through pages.
  • Reporting of visibility of advertisements in order to calculate ad revenues.
  • Deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result.

Implementing intersection detection in the past involved event handlers and loops calling methods like Element.getBoundingClientRect() to build up the needed information for every element affected. Since all this code runs on the main thread, even one of these can cause performance and problems. When a site is loaded with these tests, things can get downright ugly.

Consider a web page that uses infinite scrolling. It uses a vendor-provided library to manage the advertisements placed periodically throughout the page, has animated graphics here and there, and uses a custom library that draws notification boxes and the like. Each of these has its own intersection detection routines, all running on the main thread. The author of the web site may not even realize this is happening, since they're using two libraries that they may know very little about the inner workings of. As the user scrolls the page, these intersection detection routines are firing constantly during the scroll handling code, resulting in an experience that leaves the user frustrated with the browser, the web site, and their computer.

The Intersection Observer API lets code register to have a callback function executed whenever the element they wish to monitor enters or exits another element (or the viewport), or when the amount by which the two intersect changes by a requested amount. This way, sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimize the management of intersections as it sees fit.

One thing the Intersection Observer API can't tell you: the exact number of pixels that overlap or specifically which ones they are; however, it covers the much more common use case of "If they intersect by somewhere around N%, I need to do something."

Intersection observer concepts and usage

The Intersection Observer API allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element; for the purpose of this API, this is called the root element or root. Typically, you'll want to watch for intersection changes with regard to the document's viewport (which is done by specifying null as the root element ). Whether you're using the viewport or some other element as the root, the API works the same way, executing a callback function you provide whenever the visibility of the target element changes so that it crosses desired amounts of intersection with the root.

The degree of intersection between the target element and its root is the intersection ratio. This is a representation of the percentage of the target element which is visible as a value between 0.0 and 1.0.

Creating an intersection observer

Create the intersection observer by calling its constructor and passing it a reference to a callback function to be run whenever a threshold is crossed in one direction or the other:

var options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0

var observer = new IntersectionObserver(callback, options);

A threshold of 1.0 means that when 100% of the target is visible within the element specified by the root option, the callback is invoked.

Intersection observer options

The options object passed into the InterfaceObserver() constructor let you control the circumstances under which the observer's callback is invoked. It has the following fields:

The element that is used as the viewport for checking visiblity of the target. Defaults to the browser viewport if not specified or if null.
Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). If the root element is specified, the values can be percentages. This set of values serves to grow or shrink each side of theroot element's bounding box before computing intersections. Defaults to all zeros.
Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed. If you only want to detect when visibility passes the 50% mark, you can use a value of 0.5. If you want the callback run every time visibility passes another 25%, you would specify the array [0, 0.25, 0.5, 0.75, 1]. The default is 0 (meaning as soon as even one pixel is visible, the callback will be run). A value of 1.0 means that the threshold isn't considered passed until every pixel is visible.

Targeting an element to be observed

Once you have created the observer, you need to give it a target element to watch:

var target = document.querySelector('#listItem');

Whenever the target meets a threshold specified for the IntersectionObserver, the callback is invoked. The callback looks something like this:

var callback = function(entries, observer) { 
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element.

Be aware that your callback is executed on the main thread. It should operate as quickly as possible; if anything time-consuming needs to be done, use Window.requestIdleCallback().

How intersection is calculated


The intersection root and root margin

Before we can track the intersection of an element with a container, we need to know what that container is. That container is the intersection root, or root element. This can be either an element in the document which is an ancestor of the element to be observed, or null to use the document's viewport as the container.

The rectangle used as the bounds of the intersection root can be adjusted by setting the rootMargin when creating the IntersectionObserver. The values in rootMargin define offsets added to each side of the intersection root's bounding box to create the final intersection root bounds (which are disclosed in IntersectionObserverEntry.rootBounds).


[include diagrams]



Intersection change callbacks

When the amount of a target element which is visible within the root element crosses one of the visibility thresholds,


Provides a way to asynchronously observe changes in the intersection of a target element, with an ancestor element or with a top-level document's viewport. The ancestor or viewport is referred to as the root.
Provides information about the intersection of a particular target with the observers root element at a particular time. New instances of this interface cannot be created, but a list of them is returned by IntersectionObserver.takeRecords().

A simple example


<div id="box">
  <div class="vertical">
    Welcome to <strong>The Box!</strong>


#box {
  background-color: rgba(40, 40, 190, 255);
  border: 4px solid rgb(20, 20, 120);
  transition: background-color 1s, border 1s;
  width: 350px;
  height: 350px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;

.vertical {
  color: white;
  font: 32px "Arial";

.extra {
  width: 350px;
  height: 350px;
  margin-top: 10px;
  border: 4px solid rgb(20, 20, 120);
  text-align: center;
  padding: 20px;


Finally, let's take a look at the JavaScript code that uses the Intersection Observer API to make things happen.

Setting up

First, we need to prepare some variables and install the observer.

var numSteps = 20.0;

var boxElement;
var prevRatio = 0.0;
var increasingColor = "rgba(40, 40, 190, ratio)";
var decreasingColor = "rgba(190, 40, 40, ratio)";

// Set things up.

window.addEventListener("load", function(event) {
  boxElement = document.querySelector("#box");

}, false);

The constants and variables we set up here are:

A constant which indicates how many thresholds we want to have between a visibility ratio of 0.0 and 1.0.
This variable will be used to record what the visibility ratio was the last time a threshold was crossed; this will let us figure out whether the target element is becoming more or less visible.
A string defining a color we'll apply to the target element when the visibility ratio is increasing. The word "ratio" in this string will be replaced with the target's current visibility ratio, so that the element not only changes color but also becomes increasingly opaque as it becomes less obscured.
Similarly, this is a string defining a color we'll apply when the visibility ratio is decreasing.

We call Window.addEventListener() to start listening for the load event; once the page has finished loading, we get a reference to the element with the ID "box" using querySelector(), then call the createObserver() method we'll create in a moment to handle building and installing the intersection observer.

Creating the intersection observer

The createObserver() method is called once page load is complete to handle actually creating the new IntersectionObserver and starting the process of observing the target element.

function createObserver() {
  var observer;

  var options = {
    root: null,
    rootMargin: "0px",
    threshold: buildThresholdList()

  observer = new IntersectionObserver(handleIntersect, options);

This begins by setting up an options object containing the settings for the observer. We want to watch for changes in visibility of the target element relative to the document's viewport, so root is null. We need no margin, so the margin offset, rootMargin, is specified as "0px". This causes the observer to watch for changes in the intersection between the target element's bounds and those of the viewport, without any added (or subtracted) space.

The list of visibility ratio thresholds, threshold, is constructed by the function buildThresholdList(). The threshold list is built programmatically in this example since there are a number of them and the number is intended to be adjustable.

Once options is ready, we create the new observer, calling the IntersectionObserver() constructor, specifying a function to be called when intersection crosses one of our thresholds, handleIntersect(), and our set of options. We then call observe() on the returned observer, passing into it the desired target element.

We could opt to monitor multiple elements for visibility intersection changes with respect to the viewport by calling observer.observe() for each of those elements, if we wanted to do so.

Building the array of threshold ratios

The buildThresholdList() function, which builds the list of thresholds, looks like this:

function buildThresholdList() {
  var thresholds = [];

  for (var i=1.0; i<=numSteps; i++) {
    var ratio = i/numSteps;

  return thresholds;

This builds the array of thresholds—each of which is a ratio between 0.0 and 1.0, by pushing the value i/numSteps onto the thresholds array for each integer i between 1 and numSteps. It also pushes 0 to include that value. The result, given the default value of numSteps (20), is the following list of thresholds:

# Ratio # Ratio
1 0.05 11 0.55
2 0.1 12 0.6
3 0.15 13 0.65
4 0.2 14 0.7
5 0.25 15 0.75
6 0.3 16 0.8
7 0.35 17 0.85
8 0.4 18 0.9
9 0.45 19 0.95
10 0.5 20 1.0

We could, of course, hard-code the array of thresholds into our code, and often that's what you'll end up doing. But this example leaves room for adding configuration controls to adjust the granularity, for example.

Handling intersection changes

When the browser detects that the target element (in our case, the one with the ID "box") has been unveiled or obscured such that its visibility ratio crosses one of the thresholds in our list, it calls our handler function, handleIntersect():

function handleIntersect(entries, observer) {
  entries.forEach(function(entry) {
    if (entry.intersectionRatio > prevRatio) { = increasingColor.replace("ratio", entry.intersectionRatio);
    } else { = decreasingColor.replace("ratio", entry.intersectionRatio);

    prevRatio = entry.intersectionRatio;

For each IntersectionObserverEntry in the list entries, we look to see if the entry's intersectionRatio is going up; if it is, we set the target's background-color to the string in increasingColor (remember, it's "rgba(40, 40, 190, ratio)"), replaces the word "ratio" with the entry's intersectionRatio. The result: not only does the color get changed, but the transparency of the target element changes, too; as the intersection ratio goes down, the background color's alpha value goes down with it, resulting in an element that's more transparent.

Similarly, if the intersectionRatio is going up, we use the string decreasingColor and replace the word "ratio" in that with the intersectionRatio before setting the target element's background-color.

Finally, in order to track whether the intersection ratio is going up or down, we remember the current ratio in the variable prevRatio.


Below is the resulting content. Scroll this page up and down and notice how the appearance of the box changes as you do so.


Specification Status Comment
Intersection Observer Editor's Draft Initial definition.

Browser compatibility

Feature Chrome Edge Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Basic support 51 15 55 (55)[1][2] No support 38 ?
Feature Android Webview Chrome for Android Firefox Mobile (Gecko) Firefox OS IE Mobile Opera Mobile Safari Mobile
Basic support 51 51 55.0 (55)[1][2] No support No support 38 ?

[1] This feature has been implemented since Gecko 53.0 (Firefox 53.0 / Thunderbird 53.0 / SeaMonkey 2.50) behind the preference dom.IntersectionObserver.enabled, which was false by default. Enabled by default beginning in Firefox 55. See bug 1243846.

[2] Firefox doesn't currently take the clip-path of ancestor elements into account when computing the visibility of an element within its root. See bug 1319140 for the status of this issue.

See also

Document Tags and Contributors

 Last updated by: Sheppy,