First published October 23, 2022

Rely on JavaScript's Intersection Observer to Execute Code When in View (or Not)

Stop a video playing or animate an element onscreen - the Intersection Observer API is highly versatile.

Overhead time lapse image of intersection at night

It's the small things that matter

When dealing with the business of building websites on a daily basis, I can't help but start to notice that what separates the good sites from the great ones, is that the great ones pay close attention to the small details.

Little page animations, a button that disables after form submission, or when elements on the page only reveal themselves when the user scrolls to that portion of the page. You might not immediately recognize that this sort of nicety is there to improve the user experience, but it becomes more obvious when it's missing: a video that won't stop playing even after you've scrolled away, an error message that you can't see until you scroll up the page from the submit button that failed, there's a million little annoyances like this.

Recently, I was building a new About Us page for my company's marketing site, and in the mockup the designer gave me, there were some cool animations that showed a series of cards toward the bottom of the page that slid into view only when the user had scrolled far enough down the page to see them. It looked really slick, and it turns out that nowadays just a little CSS and JavaScript was all that was needed to make this possible, and a host of other handy interactions.

Today, I'll show you how the JavaScript Intersection Observer API can easily control how elements react based on their visibility in the viewport.

Below is a video showing how the cards at the bottom of this page don't animate and slide into view until the viewer has reached them: that's one example of Intersection Observer at work.

Intersection Observer

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

Put simply: it can tell if an element is in view or not, and cause things to act accordingly.

Before Intersection Observer existed this type of intersection detection was clunky with lots of event handlers and loops in the main thread, potentially causing performance problems as well as cluttered code.

An intersectionObserver, on the other hand, is created and assigned a callback function that runs whenever a threshold is crossed in one direction or the other for a particular element in the DOM that it is assigned to "observe."

It can also be given an optional series of options that tell it when to invoke the callback function:

  • The root - the element used as the viewport for checking visibility of the target.
  • The rootMargin - a set of values around the root element's bounding box to grow or shrink before computing intersections.
  • The threshold - a number that indicates at what percentage of the target's visibility the observer's callback function should be executed. (For example, the default 0 means as soon as even one pixel is visible the callback will run, and 1.0 means the threshold isn't considered passed until every pixel of an element is visible).

There's plenty more nuance to how the intersection observer can be fine tuned and leveraged to cool effect, but what I've covered above should be enough to help you follow the examples I'll be demoing. If you'd like to learn more, I'd recommend reading the docs from Mozilla, which include some great code examples to play with.

All of this writing may not make perfect sense yet, so let's look at some code examples where you can see intersection observers in action.

Intersection Observer code examples

There's a couple of different scenarios where intersection observer is being used on the marketing site in unique ways, and I'm going to show both of them to you to help demonstrate how flexible it can be.

Run animations only when the user will see the result

The cards with customer quotations designed to slide into view with the help of intersection observer

These cards slide into view once the user can see them on screen with the help of intersection observer.

The first example I'll show you is the one I described in the introduction to this blog: it animates a series of cards on-screen, but only when the user has scrolled down the page far enough to view the cards.

NOTE:

The site code I'm referencing is written in Hugo, a popular, open source, static site generator written in Go. Like many SSGs, it relies on templates to render the majority of the site's HTML, and for the purposes of clarity in this article, I've replaced Go variables injected into the template with the generated HTML.

Let's look first at the HTML and JavaScript for the three cards being rendered in the page. This code snippet shows just one of the cards, but it's the same for all of them.

quote-cards.html

<div class="row">
    <div class="col-lg-4 card-group customer-quote-card">
      <div class="card">
        <div class="card-body">
          <p class="customer-quote-text">
            <img 
              class="customer-quote-img" 
              src="/images/about/quote-left-solid.svg" 
              alt="decorative left quotation mark"
            />
            "This is a quote here."
            <img
              class="customer-quote-img"
              src="/images/about/quote-right-solid.svg" 
              alt="decorative right quotation mark"
            />
          </p>
          <p class="customer-quote-source">
            Source of the quote
          </p>
          <p class="customer-quote-source-role">
            Role and company the source works for
          </p>
        </div>
      </div>
    </div>
  <script type="text/javascript">
    /* this code keeps the cards from animating into view until a user's scrolled down far enough to see them */
    var quoteCard = document.querySelectorAll(".customer-quote-card");

    var observerOptions = {
      threshold: 0.7,
    };

    var callback = (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add("animated");
        }
      });
    };

    var quoteCardObserver = new IntersectionObserver(callback, observerOptions);

   // loop through each card available and add this observer to it
    quoteCard.forEach((card) => {
      quoteCardObserver.observe(card);
    });
  </script>
</div>

The HTML in the snippet outlines the card element and the details contained within that card: the quote, the name, and the company and role of the person being quoted.

The JavaScript at the bottom of the snippet is where intersection observer comes into play. First, a variable named quoteCard is created to target the customer-quote-card CSS class on the div that surrounds each card.

Then, the observerOptions variable is declared with a threshold of 0.7 - this will be passed to the IntersectionObserver object shortly, and will require that at least 70% of the card be visible before animating it into view.

A callback() function is defined, which loops through a list of items and for each item, if the item isIntersecting (a boolean value which is true if the target element intersects with the intersection observer's root), the animated class is added to that item's CSS classes. Essentially, this function will add the CSS class to each card necessary to trigger the animation and bring them into view on the page when the isIntersecting threshold is reached.

Next, a new IntersectionObserver instance is instantiated as quoteCardObserver and the callback() and observerOptions are passed in.

Finally, for each quoteCard object, the quoteCardObserver function is attached to the card.

With the HTML and JavaScript is set up, it's time to add the CSS (remember that animated CSS class mentioned earlier?) to animate the cards on screen when the intersection observer's callback() function fires.

about.scss

.card-group {
  opacity: 0;
}

.animated {
  animation: slideLeft 1.5s;
  animation-delay: 0s;
  animation-fill-mode: backwards;
  opacity: 1;
}

@keyframes slideLeft {
  0% {
    transform: translateX(100%);
    opacity: 0;
  }

  100% {
    transform: translateX(0);
    opacity: 1;
  }
}

In the SCSS code, the card-group class starts out with an opacity of 0 to keep the cards hidden from view.

The animated class is created with an animation property designating the slideLeft keyframe with a duration of 1.5 seconds.

In addition to the animation property, the animation-delay, animation-fill-mode and opacity are also defined on this class.

  • animation-delay: 0; ensures the animation will play immediately,
  • animation-fill-mode: backwards; means the element will apply the values defined in the 0% keyframe as soon as it is applied to the target (i.e. the card will stay hidden with an opacity of 0 as soon as it's rendered in the DOM).
  • opacity: 1; once the slideLeft keyframe animation has ended, the card will have 100% visibility on-screen.

And last but not least, the @keyframes animation sequence is defined. There are only two keyframes defined (0% and 100%) to indicate the cards start with no opacity (opacity: 0;) and 100% off the screen to the right (transform: translateX(100%);, and when it ends they'll be completely visible (opacity: 1;) and on screen (transform: translateX(0);).

In the end, it produces this effect:

NOTE:

CSS animations are beyond the scope of this tutorial, but if want to study them in more depth, I'd recommend starting here - they're very cool!

Great! That's one example of how intersection observer can be used to control animation timing so a user will see it. Now let's consider another option.

Play a video only when the user can see it

Video modal that only plays the video while the modal is visible in the viewport.

This video will only play when it is visible to the user and they click the play button. If user dismisses the modal by clicking somewhere else on the page, the modal will hide and video stop playing.

The second example I'll share concerns a video modal that stops playing when the modal is not visible. When a button is clicked, the modal does a full page takeover where the video player sits at the center of the viewport. If the user clicks somewhere besides the video while it's playing, the modal is dismissed and hidden from view and the video will stop playing. This is possible because of intersection observer.

Let's take a look at the code that makes this happen.

video_modal.html

<div class="modal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button
          type="button"
          class="close"
          data-dismiss="modal"
          aria-label="Close"
        >
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body embed-responsive-16by9">
        <iframe
          width="560"
          height="315"
          src="https://www.youtube.com/embed/4q1qqzDzJeQ"
          title="YouTube video player"
          frameborder="0"
          allowfullscreen
        ></iframe>
      </div>
    </div>
  </div>
  <script type="text/javascript">
    var iframe = document.querySelector("iframe");
    var ytsrc = iframe.src;

    var observer = new IntersectionObserver(
      function (entries) {
        // isIntersecting is true when element and viewport are overlapping
        // isIntersecting is false when element and viewport don't overlap
        if (entries[0].isIntersecting === false) iframe.src = "";
        else iframe.src = ytsrc;
      }
    );

    observer.observe(iframe);
  </script>
</div>

In the code above, a video modal and an iframe video embed from YouTube are created in the HTML.

In the JavaScript directly below it, an iframe variable is defined focused on the <iframe> HTML element, and a ytsrc variable is defined to keep track of the the iframe's video source - this will be used inside the intersection observer's callback.

Next, a new intersection observer is initialized (observer), and this one's callback function iterates through the list of entries it receives, and if the first entry in the list is not intersecting (i.e. the video iframe and the viewport aren't overlapping, or rather, the modal's not visible to the viewer), the iframe's src is set to an empty string so no video URL is available to play. If the entry is intersecting (i.e. the video modal is visible in the viewport), the iframe's src is set to the ytsrc variable: the actual YouTube video URL.

Finally, the new observer object is told to observe the iframe variable, so whenever the iframe is in the viewport (e.g. when a user clicks a button to open the modal), its video source is the YouTube video. Whenever that changes (and the modal is hidden from view), the video URL is set to an empty string.

And this results in no video continuing to play when the video modal is dismissed.

To see how it works, watch this video with the sound on to hear how the audio ceases when the modal is dismissed and hidden from view.

Pretty useful, huh?

Conclusion

Small details can make for great user experiences, whether it's a video that stops playing once a user's scrolled past it or an animation that only happens when the elements are in view for a user.

While these little interactions once required a good deal of extra code and awareness of how long the main thread might be blocked, the introduction of the Intersection Observer API simplified things dramatically. Now, a single object allows a developer to specify an element to observe, a callback function to do something, and even fine tuned the function to only fire when certain conditions are met. It's quite useful in many scenarios, actually.

Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.

Thanks for reading. I hope this demo of how to use the Intersection Observer API helps you out in your future endeavors. I know I really appreciate great website experiences, and it's always good to have another tool in the arsenal to make them possible.

References & Further Resources

Want to be notified first when I publish new content? Subscribe to my newsletter.