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 IntersectionObserver. It is now supported in Chrome, Firefox, Opera and Edge and there is a good polyfill for it.

I decided to experiment and push IntersectionObserver to its limits. Check it out here.

NOTE: 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.

Grid of cards changing visibility on scroll with IntersectionObserver.

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 IntersectionObserver 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.