Skip to content

Software Engineer at Heydoc

The Intersection Observer API explained

Detecting which elements are visible within the current viewport’s boundaries has always been a tricky and expensive task in terms of performance. Lazy loaded images on Medium.com, infinite scrolling pictures of vegan food on Pinterest or animated images on every f*****g Tumblr theme are just a few examples where this functionality can be found in abundance. The reporting of ad “visibility” for monetizing purpose is another important use case. There’s good news on the horizon though! The web platform doesn’t have to struggle to do all these things manually anymore — The Intersection Observer API just landed in Chromium 51. It allows us to do these things with ease, reduce CPU usage, increase battery life and eliminate rendering junk.

Bare in mind that we are talking about a really new API. At the moment of writing this article the browser support is restricted to Google Chrome 51 and Opera 38. If you want to play around with it in older browsers give some thanks to Surma for this great polyfill.

IntersectionObserver in Google Chrome Canary

How to use Intersection Observer API #

You don’t have to declare a listener that on every single scroll event triggers some crazy getBoundingClientRect() calculations anymore. The new API is much nicer to use and read. Simply create a new instance of IntersectionObserver that takes two arguments — a callback function and an optional options object. Trigger the observe method and pass in the element that should be watched. When the element enters or exits the viewport, the callback function will be fired.

var watchMe = new IntersectionObserver(callback, options);
watchMe.observe(elm);

By default, the callback function will be fired whenever an element appears and leaves the viewport. The function returns an array of IntersectionObserverEntry objects and each of them contains properties about each element that has been shown on the viewport (boundingClientRect, intersectionRatio, intersectionRect, rootBounds, target and time).

The second parameter (options) allows you to specify some settings by passing the IntersectionObserverInit object. You can change the context (root) that defaults to null which is document’s viewport, the amount of margin from the context’s boundaries (rootMargin) with a default value 0px, and finally an array of threshold which is a list of thresholds at which to trigger the callback.

If you need to observe more than one element, simply call the observe method multiple times.

Demo time #

Have you ever heard this quote by Stephen R. Covey from “The 7 Habits of Highly Effective People”?

To learn and not to do is really not to learn. To know and not to do is really not to know.

Demo time! I have created a list of paragraphs. Initially all of them are scaled down and see through. When a paragraph passes the viewport’s edge by half of it’s height (threshold: [0.5]) then it animates to its regular size and full opacity. Maybe it’s not especially creative, but it does the job and allows you to copy/paste my code or extend it to make some super funky stuff :-)

// the callback function that will be fired
// when the element apears in the viewport
function onEntry(entry) {
  entry.forEach((change) => {
    change.target.classList.add('visible');
  });
}

// list of options
let options = {
  threshold: [0.5]
};

// instantiate a new Intersection Observer
let observer = new IntersectionObserver(onEntry, options);

// list of paragraphs
let elements = document.querySelectorAll('p');

// loop through all elements
// pass each element to observe method
// ES2015 for-of loop can traverse through DOM Elements
for (let elm of elements) {
  observer.observe(elm);
}

See the Pen Intersection Observer API explained by Pawel Grzybek (@pawelgrzybek) on CodePen.

Hopefully this has helped you out. Don’t be shy and please share your experiments with using the Intersection Observer API. Any question? Please use the comments section below. If you liked this article please don’t hesitate to use the share button. Thanks!

Comments

  • Š
    Šime Vidas

    Can it work the other way around, i.e. shrink the paragraphs as they leave the viewport? I feel, the effect isn’t complete without both.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Of course you can :) This is just a quick example that I made in 5 minutes. It was not all about the effect here, just about concept. The callback function is fired whenever the element enters and exits viewport. In this case the only thing that you need to do is replace `classList.add()` with`classList.toggle()` to remove a class when it leaves the viewport.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • A
    Alexander Wallin

    I'm not sure whether the API has changed, but the embedded pen is not working. The onEntry callback is called for all nodes when the page loads, so there needs to be a check on the isIntersecting property when setting the visible class.

    Cheers for a good read!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Great spot! You are probably right — API changed as the example is not working anymore but I'm 100% it worked fine. I added review of this post to my list to do and I will update you whenever the article is updated.

      Thanks a ton for reporting and have a lovely day 🥑

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      OK, so you were right... The API changed and the post will be amended in the next few days. Thanks a lot for reporting an issue again 🍻

      https://twitter.com/DasSurm...

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • A
        Alexander Wallin

        Glad to help!

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
        • Pawel Grzybek
          Pawel Grzybek

          Hi @disqus_KxxmClHY2C:disqus. Based on Surma's advice I fixed the snippet. Because the callback function is invoked on initialization I added a condition to check for `isIntersecting` property. If this one passes the condition — class is added. Thanks a lot again!

          👆 you can use Markdown here

          Your comment is awaiting moderation. Thanks!
  • M
    Marouen Mhiri

    Thx for the article. Is there a possibility to set a negativ Threshold so that the element become the class
    visible' when they are i.e. 200px below the viewport?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • s
      screeny05

      jep. have a look at the rootMargin-option.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • M
        Marouen Mhiri

        Already tried it out! but this seems to have an effect of the first intersectionObservers! Posted a question Stackoverflow, but still not have a response! I think this feature still buggy

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
  • N
    Nico

    Hi Powel

    Thanks for this great article, very informative!

    Is there a possibility to track live elements in the DOM?

    for example... Let's say we are tracking 10 div's and onChange of a checkbox the user can remove one or more div's from the DOM, then later re-insert them if they like.

    Will the observer register first the reduced number of divs?

    and...

    then the increased number of div's and update accordingly?

    if not is there a way to do so?
    If not is this something that will change in the future?
    If not is there a poly fill which can provide this functionality?

    Thank you in advance,
    Nico

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • A
    Alex Carletto

    Hi! thanks for the article.
    I'm trying to get it work on Safari and iOS but i'm stucked. Other browsers and device ok.
    Here is how i declare the Observer:
    function createObserver() {
    var observer;
    var options = {
    root: null,
    rootMargin: "0px",
    threshold: buildThresholdList()
    };
    observer = new IntersectionObserver(handleIntersect, options);
    observer.observe(boxElement);
    }

    Here the handleIntersect function:
    function handleIntersect(entries, observer) {
    entries.forEach(function(entry) {
    if (entry.intersectionRatio > prevRatio) {
    console.log("VIDEO IN");
    p.style.position = "relative";
    } else if(entry.intersectionRatio === 0 && scrolled === 1 ) {
    console.log("VIDEO OUT");
    p.style.position = "fixed";
    }
    });
    }

    My goal is to change position to a video element if the user scrolls further to the video element.
    I tried also to use a setTimeout in handleIntersect() but nothing changes.
    Can you help me? Thanks in advance

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Hi. I mentioned that on the article that this is a bleeding edge API and browser support can vary across vendors. Please have a look at the compatibility table first.

      https://developer.mozilla.o...

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!