Animating Elements When They Appear In Viewport

Animating elements as they appear on scroll is becoming a common feature on modern websites and is a great way to enhance UX.

With Javascript, we can determine an element’s visibility by finding its position and dimensions relative to the viewport’s size and scroll position. If we know an element is visible, we simply add a new class to it and leverage CSS to animate the element any way we choose. Once the element goes outside the fold, we remove the class.

The advantage of having a simple class toggle is to keep things flexible and performant; using Javascript to animate DOM elements isn’t as performant as CSS animation, nor is it practical to apply animation styles inline on each element, so leveraging classes and managing animations from our CSS is better.

The dangers of implementation

Firstly, the whole premise of this feature hinges on scroll (and to some extent resize) events. This can result in expensive reflow operations. The calculations need to be constantly updated whenever a user scrolls or adjusts the size of the browser window, and the effect on performance can be huge.

The second factor is the accuracy of visibility. Many examples I’ve seen utilize getBoundingClientRect, an API to get the size and position of an element’s bounding box relative to the viewport. However, this has known issues in Safari, Chrome, and IE11 or less. Furthermore, performance tests comparing the speed to retrieve values via getBoundingClientRect and Offset show the latter to be faster. Other methods rely only on downward scrolling alone and not other directions.

Finally, it’s important to ensure that this feature is an enhancement and we should only allow it to run if certain Javascript APIs an CSS3 attributes are supported. Implementation without feature testing could result in unnecessary performance hindrance in legacy browsers, or worse, show initial unfinished states (such as zero visibility) on elements.

Starting from scratch

Create a feature detection

Up front we need to make sure some API features such as requestAnimationFrame and classList will Cut The Mustard. We can potentially polyfill for both of these if we need wider support. The condition looks like this:

if (window.requestAnimationFrame && document.documentElement.classList) {
    document.documentElement.classList.add('enhanced');

	// Do stuff
}

Basically, if the browser supports these API features, we add the class “enhanced” to the html tag. We then use this class as a selector on our enhanced / animated CSS.

Implement a helper function for throttling

Up front we want to provide a throttle — like this one from Underscore.js; which will provide better performance when we listen for events such as scroll or resize. It works by returning a new, throttled version of a passed function, that, when invoked repeatedly, will only call the original function at most once at every specified millisecond value.

Create a variable for requestAnimationFrame

requestAnimationFrame is a browser API that allows us to leverage the browser’s best recommendation to schedule and optimize animations appropriately, essentially so they’re smoother. This variable includes all the prefixes and will come into play later:

var _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame;

Create a variable selector for all revealing elements

We’re going to create a single class that can be used multiple times in the DOM, as a means to detect which elements should be watched:

var revealer = document.querySelectorAll('.revealer');

Get the viewport dimensions

There are a number of items we need that I mentioned earlier which we’ll break out into functions. The first is getting the viewport window dimensions. The following will deliver reliable values:

var getViewportSize = function() {
    return {
        width: window.document.documentElement.clientWidth,
        height: window.document.documentElement.clientHeight
    };
};

Get the current scroll position

The following function will get the current scroll position on both x and y axis:

var getCurrentScroll = function() {
    return {
        x: window.pageXOffset,
        y: window.pageYOffset
    };
};

Get the element’s dimensions and position

Leveraging offset, this function returns the element’s dimensions, and the distance through all its parents to the very top and left of the document.

var getElemInfo = function(elem) {
    var offsetTop = 0;
    var offsetLeft = 0;
    var offsetHeight = elem.offsetHeight;
    var offsetWidth = elem.offsetWidth;

    do {
        if (!isNaN(elem.offsetTop)) {
            offsetTop += elem.offsetTop;
        }
        if (!isNaN(elem.offsetLeft)) {
            offsetLeft += elem.offsetLeft;
        }
    } while ((elem = elem.offsetParent) !== null);

    return {
        top: offsetTop,
        left: offsetLeft,
        height: offsetHeight,
        width: offsetWidth
    };
};

Check visibility of the element in the viewport

Now we can create a function that calculates if the element is within the boundary of the window. The following is actually based on a method that is integral to ScrollReveal, a popular plugin that renders inline styles to revealing elements. Essentially we’re checking the boundaries of the element and the boundaries of the window, and checking if the element is within the boundary of the window.

We can also provide a space offset, or cushion, to allow the element to be revealed only if it’s showing a minimum amount. In the example below that would be at least 20% (0.2).

var checkVisibility = function(elem) {
    var viewportSize = getViewportSize();
    var currentScroll = getCurrentScroll();
    var elemInfo = getElemInfo(elem);
    var spaceOffset = 0.2;
    var elemHeight = elemInfo.height;
    var elemWidth = elemInfo.width;
    var elemTop = elemInfo.top;
    var elemLeft = elemInfo.left;
    var elemBottom = elemTop + elemHeight;
    var elemRight = elemLeft + elemWidth;

    var checkBoundaries = function() {
        // Defining the element boundaries and extra space offset
        var top = elemTop + elemHeight * spaceOffset;
        var left = elemLeft + elemWidth * spaceOffset;
        var bottom = elemBottom - elemHeight * spaceOffset;
        var right = elemRight - elemWidth * spaceOffset;

        // Defining the window boundaries and window offset
        var wTop = currentScroll.y + 0;
        var wLeft = currentScroll.x + 0;
        var wBottom = currentScroll.y - 0 + viewportSize.height;
        var wRight = currentScroll.x - 0 + viewportSize.width;

        // Check if the element is within boundary
        return (top < wBottom) && (bottom > wTop) && (left > wLeft) && (right < wRight);
    };

    return checkBoundaries();
};

Create a loop to add a custom class

This function will loop through all objects for the revealer element, check our checkVisibility function and add or remove a new “revealed” class depending on if it returns true or false.

var toggleElement = function() {
    for (var i = 0; i < revealer.length; i++) {
        if (checkVisibility(revealer[i])) {
            revealer[i].classList.add('revealed');
        } else {
            revealer[i].classList.remove('revealed');
        }
    }
};

Throttling events with requestAnimationFrame

Finally, here we have a cross-browser method for handling both scroll and resize events, and then a combo of throttling and rAF to execute our toggleElement function.

var scrollHandler = throttle(function() {
    _requestAnimationFrame(toggleElement);
}, 300);

var resizeHandler = throttle(function() {
    _requestAnimationFrame(toggleElement);
}, 300);

if (window.addEventListener) {
    addEventListener('scroll', scrollHandler, false);
    addEventListener('resize', resizeHandler, false);
} else if (window.attachEvent) {
    window.attachEvent('onscroll', scrollHandler);
    window.attachEvent('onresize', resizeHandler);
} else {
    window.onscroll = scrollHandler;
    window.onresize = resizeHandler;
}

That’s all folks!

See It In Action

See the Pen Animate Elements If In Viewport by Christian Miller (@xtianmiller) on CodePen.

Now the fun begins

With our script in place, we can concentrate on actual animations. I’m sure I will be exploring and covering some of that in future posts!

Update

I’ve since created my very own plugin; Emergence.js, which essentially incorporates some of the principles in this article but with a number of differences, such as leveraging HTML5 data-* attributes instead of classes for toggling the state of the element.

By Christian Miller  |  Follow me on Twitter