Skip to content
Jonathan Harrell

Article tags

  • javascript

Controlling Element Visibility with the Intersection Observer API

Controlling the display of elements based on their visibility within the viewport has typically been a rather messy affair, involving calculations using window height and getBoundingClientRect(). There is now a new API that makes this much simpler called Intersection Observer. It is now supported in Chrome, Firefox, Opera and Edge.

I decided to experiment and push IntersectionObserver to its limits.

You probably shouldn’t create as many observers on a production site as I have done. You start to run into performance issues. However, this experiment should help you visualize how the API works.

The HTML and CSS

I have a simple grid of cards that is styled using CSS grid:

<section class="card-grid">
  <div class="card"></div>
  <div class="card"></div>
  <div class="card"></div>
  ...
</section>
.card-grid {
  display: grid;
  grid-template-columns: repeat(
      auto-fill,
      minmax(100px, 1fr)
  );
  grid-gap: 45px;
}

Creating the Observers

I loop over each card and create an observer. The observer accepts a callback and an options object. Note that in options I am setting the rootMargin to a negative value. This insets the intersection point in from the viewport on all sides by 100px. So a card can be up to 100px in the viewport before the observer will read it as intersected.

I have also set the threshold option as an array with two numeric values. These are essentially the percentages of intersection at which the observer will respond. So, when a card is 50% in the viewport and when it is 100% in, the observer will fire the callback.

const options = {
  rootMargin: '-100px',
  threshold: [0.5, 1]
}
const observer = new IntersectionObserver(callback, options);

const targets = document.querySelectorAll('.card');
targets.forEach(target => observer.observe(target));

Setup the Callback

The callback function gives me an array of entries—each entry is essentially an intersection change. I can check the intersectionRatio on each entry and apply some styling appropriately.

const callback = entries => {
  entries.forEach(entry => {
    const ratio = entry.intersectionRatio;

    // look at the ratio and do stuff to each element
  });
}

I use a switch statement to apply different styling for different ratios:

switch (true) {
  case (ratio === 1):
    entry.target.style.opacity = 1;
    entry.target.style.transform = 'scale(1.25)';
    return;

  case (ratio < 1 && ratio >= 0.5):
    entry.target.style.opacity = 0.5;
    entry.target.style.transform = 'scale(1.1)';
    return;

  case (ratio < 0.5):
    entry.target.style.opacity = 0.15;
    entry.target.style.transform = 'scale(1.0)';
    return;

  default:
    return;
}

Conclusion

The Intersection Observer API provides a more straightforward and powerful method for checking element visibility relative to the viewport. Hopefully browser support continues to improve and we’ll be able to use it soon in production sites without needing a polyfill.

Subscribe

Want more front-end tips and tricks? Sign up for my newsletter to stay up-to-date.