Using the Intersection Observer API to Trigger Animations and Transitions

With rapidly increasing browser support, there’s been a lot of excitement around the Intersection Observer API, which provides an easy way to watch and register callbacks to trigger when elements on a page come into view. Probably the most obvious use case is for lazy loading image when they come into view. Using Intersection Observer makes it less resource intensive and a lot easier to implement compared to listening for scroll events and checking if an element is about to enter the viewport. Behind the scenes, the Intersection Observer API makes use of requestIdleCallback to help with performance even more.

We’ve recently touched on using Intersection Observer to lazy load images when we covered writing abstract components in Vue.js. Another fun use case is to make it easy to trigger CSS animations and transitions when an element comes into view. This is exactly what we’ll be doing here, and give you a general overview of how to use the API in general at the same time.

Setup

Using the Intersection Observer API is as simple as creating an IntersectionObserver instance and calling observe on the instance with an element to observe:

const myImg = document.querySelector('.animate-me');

observer = new IntersectionObserver(entry => {
  console.log(entry);
});

observer.observe(myImg);

With this, an InterSectionObserverEntry object will be logged to the console each time the observed element either intersects in or out of the viewport.The entry itself contains properties with data about the target, the boundingClientRect and the intersectionRatio, among others.

Most of the time, you’ll instead want to observe multiple elements at once. This can be done with the same intersection observer:

const myImgs = document.querySelectorAll('.animate-me');

observer = new IntersectionObserver(entries => {
  console.log(entries);
});

myImgs.forEach(image => {
  observer.observe(image);
});

We’re not yet detecting if the observed elements are either in or out of view, just that the intersection has been triggered. Here’s how you could act on the observed elements either entering the view or leaving the view:

const myImgs = document.querySelectorAll('.animate-me');

observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0) {
      console.log('in the view');
    } else {
      console.log('out of view');
    }
  });
});

myImgs.forEach(image => {
  observer.observe(image);
});

We simply check if the entry’s intersectionRatio is greater than 0 to know if it has entered the viewport.

Unobserving

Say you want to observe an element entering the view once and then stop observing that element. This is the case with lazy loading, where after image has entered the view and has been loaded, you don’t need to observe it anymore. This can easily be done with observer.unobserve.

Here for example, when the element enters the view, the in the view message logs and then we stop observing that element so the message won’t log again:

const myImgs = document.querySelectorAll('.animate-me');

observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0) {
      console.log('in the view');
      observer.unobserve(entry.target);
    } else {
      console.log('out of view');
    }
  });
});

myImgs.forEach(image => {
  observer.observe(image);
});

Once all the observed elements have been unobserved, the observer won’t be triggered and nothing will log to the console anymore.

You can also call observer.disconnect() at any time to completely stop observing all the observed elements.

Configuration

You can pass-in a configuration object as a second argument when instantiating an IntersectionObserver and the following keys can be configured:

  • root: The element to use for intersection checking. It defaults to the document, but you may want to change the default for something like an iframe for example.
  • rootMargin: A string with values in the same format as for a CSS margin or padding value. For example: '3rem 2rem'. This creates a margin of the specified size around the root element, to effectively create an inset or an outset for the intersection point. It defaults to '0px'.
  • threshold: An array of number values between 0 and 1. The values correspond to the ratio of visibility of the element, with 0 being completely out of view and 1 being fully in the view. If you provide multiple values, the intersection callback will be called when each specified threshold value is reached. It defaults to [0].

Here’s how you can pass-in some configuration:

const config = {
  rootMargin: '50px 20px 75px 30px',
  threshold: [0, 0.25, 0.75, 1]
};

observer = new IntersectionObserver((entry, config) => {
  // ...
});

Simple Example

Below you’ll see an example where we have two images elements being observed. A facy class gets added or removed depending on if the image is in or out of the viewport.

Here’s the code to implement that example:

const images = document.querySelectorAll('.animate-me');

observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0) {
      entry.target.classList.add('fancy');
    } else {
      entry.target.classList.remove('fancy');
    }
  });
});

images.forEach(image => {
  observer.observe(image);
});

And the rest of the magic is done with some simple CSS:

.cowboy.fancy {
  animation: anim1 .7s ease-out;
}
.chef.fancy {
  animation: anim2 .7s ease-out;
}

@keyframes anim1 {
  0% {
    opacity: 0;
    transform: translateX(-30rem) rotate(-45deg);
  }
  100% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
}

@keyframes anim2 {
  0% {
    opacity: 0;
    transform: translateX(30rem) rotate(45deg);
  }
  100% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
}

Browser Support

Can I Use intersectionobserver? Data on support for the intersectionobserver feature across the major browsers from caniuse.com.

✨ And it's as simple as that! Much simpler than performing manual checks on scroll events. Since it's not supported everywhere just yet, you'll probably want to use the polyfill as well for now.

✖ Clear

🕵 Search Results

🔎 Searching...