Email iconarrow-down-circleGroup 8Path 3arrow-rightGroup 4arrow-rightGroup 4Combined ShapeUntitled 2Untitled 2ozFill 166crosscupcake-icondribbble iconGroupPage 1GitHamburgerPage 1Page 1LinkedInOval 1Page 1Email iconphone iconPodcast ctaPodcastpushpinblog icon copy 2 + Bitmap Copy 2Fill 1medal copy 3Group 7twitter icontwitter iconPage 1

At a recent assembling of the greatest minds at UVD we challenged ourselves to make some small but hopefully noticeable improvements to our site and one area that came under attention was how we presented our ‘featured’ projects on our homepage.

We were showing them one at a time hidden in a ‘carousel’ style control, allowing people to cycle through them.

They were crying out to be broken free of the carousel to show off more of our work to potential clients, so we quickly decided on a simple redesign to allow this to happen. The end result has the projects animate into view on scroll, like this:

From a development perspective there’s nothing overly revolutionary here: as one of the projects enters the viewport, an animation is triggered to bring it into focus and conversely as it leaves the viewport an animation is triggered to make the project lose focus. This is quite a common pattern and quite a few libraries enable you to do this easily.

These libraries allow you to trigger an action on an element when it reaches a particular point in the viewport. Under the hood these libraries are essentially attaching a scroll event listener to the window, and on scroll any elements that you’ve registered to be checked and properties read from them (with something such as element.getBoundingClientRect) to determine their position relative to the viewport in order to decide whether it’s in view.

This approach works, however there are some performance trade offs when reading dimension properties from the DOM so regularly which can cause the page to reflow, resulting in whats commonly referred to as ‘jank’.

In addition, this is such a common pattern – to take action on something that enters (or leaves) the viewport – that surely there’s a more appropriate method that’s sympathetic to the page performance… Alas behold the Intersection Observer API, and we figured we’d put it through its paces for the UVD homepage.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

There are numerous things you can achieve with the Intersection Observer API, but let’s revisit our use case; we have three project sections, as one enters the viewport we wish for it to animate in and as it leaves it’ll animate out. In addition to this, in order for us to fully tickle the eyeballs of our users with our fancy animations we want to wait until enough of the project is visible – let’s say 25% to be in the viewport before the animation starts, and we want people to see a little bit of our exit animation, so when there’s 10% of the project remaining in the viewport we’ll trigger that animation. How could we achieve this with the Intersection Observer API?

Let’s set the scene a little bit: we have the following (abridged) DOM structure:

<div class="project">
<!-- -->
</div>
<div class="project">
<!-- -->
</div>
<div class="project">
<!-- -->
</div>

We create one new instance of an observer for the behaviour we want to trigger when an element intersects with another (by default this is the viewport).

const function onIntersectionCallback (entries, observer) {
    // .... we'll come back to this
}

const observer = new IntersectionObserver(onIntersectionCallback, {
    threshold: [0.1, 0.25]
});

The first argument to the Intersection Observer constructor is a callback that will be called when your elements intersect, we’ll revisit this in a moment. The second argument is the options, there are three options you can set: `root`, `rootMargin` and `threshold`. We’re going to focus on threshold for our example, there’s a good write up on the others on MDN.

If you remember above we wanted to trigger certain things when a percentage of our element is visible, we can achieve this with “thresholds” in our Intersection Observer options.

Threshold accepts a number or an array of numbers which is the percentage (represented as a decimal) of the element that is in the viewport before your callback will be triggered. In our example our onIntersectionCallback function will be called every time there is 10% of the element and 25% of the element visible.

Now we’ve defined the behaviour we want we now need to attach it to some elements we want to observe. We loop over our elements and pass each one to the observe function of our previously created observer.

const projects = document.querySelectorAll('.project');
Array.from(projects).forEach(project => observer.observe(project));

Now our onIntersectionCallback function will be first called when there is 10% of our element in our viewport, let’s look at that function in a bit more detail.

    function onIntersectionCallback (entries, observer) {
            entries.forEach(entry => {
                const { intersectionRatio, target } = entry;
                const ANIMATE_ENTRY_CLASS = '.animate-entry';
                const ANIMATE_EXIT_CLASS = '.animate-exit';
                const isEntering = intersectionRatio >= 0.25 && !target.classList.contains(ANIMATE_ENTRY_CLASS);
                const isExiting = intersectionRatio <= 0.1 && target.classList.contains(ANIMATE_ENTRY_CLASS);

                if (isEntering) {
                    target.classList.add(ANIMATE_ENTRY_CLASS);
                    target.classList.remove(ANIMATE_EXIT_CLASS);
                } else if (isExiting) {
                    target.classList.remove(ANIMATE_ENTRY_CLASS);
                    target.classList.add(ANIMATE_EXIT_CLASS);
                }
    
            })
    }

The callback will give you an array of entries for every intersection and a reference to the original observer, here we loop over each intersection for the element and check a couple of things based on what we want our desired behaviour to be. The entry gives us some information about the intersection;

entry.boundingClientRect
entry.intersectionRatio
entry.intersectionRect
entry.isIntersecting
entry.rootBounds
entry.target
entry.time

In our case we’ll just concern ourselves with the intersectionRatio, which is how much of our element has been intersected, and target which is a reference to the DOM element currently being intersected. You may have noticed that the callback is going to be triggered every time 25% or 10% of our element is intersected, taking a typical page scroll this is going to happen 4 times, so we need to add a little bit of state checking into our callback for it to know whether the element is scrolling into or out of view.

There are many (and more elegant ways) to do this but we’ll just check what class names have been added to our element to determine the ‘state’ it’s in. When the first 10% is visible we check whether the element has the ANIMATE_ENTRY_CLASS – which would tell us that it has already animated in and that we can treat this intersection as the element ‘exiting’ the viewport. Imaging this is the first scroll down the page this will do nothing and then we’ll hit the 25% threshold, which we can then use to animate our element in… simples.

In summary

This was a bit of an opportunity for us to experiment with a new API and we were encouraged that the polyfill available provided us with support for older browsers so there was nothing stopping us using this in production. We also found ourselves using this elsewhere so we extracted it out to a very simple focused utility library for toggling classes on elements based on thresholds which is available on github here, it’s rough but ready and thoughts/contributions are welcome.

Overall it’s great to be able to leave behind some of the old ‘work arounds’ and build upon APIs that are designed to be performant and flexible enough to be a foundation for new ideas and patterns that’ll keep the web creative and fast!

Share: