CSS animations can be a pain in the 🍑, and what's even worst is to trigger them at the exact moment. For that exact reason, and if I may be perfectly honest with you, I opt for not using them most of the time. However...
Before starting, if you need a refresher on what's an API, this is a good place to get info on it
You may think that I'm out of my mind for suggesting that you should use an API for animating CSS but hear me out... GitHub uses it to make their homepage more performant and faster so you know it must be good!
When we usually want to check an element's position in the window we might end up using stuff like elem.clientTop
, elem.offsetTop
or even elem.getBoundingClientRect()
but the truth is that these properties/methods will trigger the browser to calculate the required style and layout (check the whole list of properties that target this and a further explanation here) which creates a performance bottleneck.
A way to circumvent this is by using the Intersection Observer API, which, according to the MDN documentation, '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.'. So, basically, we'll just monitor if an element will enter/exit another element/the viewport and that's way easier for the browser to process.
The IntersectionObserver interface can be created very easily and all you have to do is to pass a callback to it and some options. The callback is what decides what will happen to the list of IntersectionObserverEntry objects and the options allow you to control the circumstances in which the callback will be called (please refer to the MDN documentation for full details on this).
For example purposes only, we'll not define our options (which will make them adopt some default values) and we'll simply work with our callback, so our initial setup would be something like this:
let expansionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.intersectionRatio > 0) { // something here } else { // something here } }) })
Now that we have our expansionObserver, we need to grab the elements that we want to animate. For this we'll use document.querySelectorAll()
and we'll get all the elements with the class .expand
.
So, on our HTML it would look like this:
<body> <div id="section-one"></div> <div id="section-two"> <div id="container"> <h1 class="expand">Hello</h1> </div> </div> </body>
And our JavaScript would look like this:
const elementsToExpand = document.querySelectorAll(".expand")
After this, we need to tell the IntersectionObserver
that we want to observe
these elements, and since we're using querySelectorAll()
we need to loop over elementsToExpand
and we'll use a forEach()
for it.
elementsToExpand.forEach(element => { expansionObserver.observe(element) })
To finish our JavaScript part, we need to fill the if/else
statement we wrote on our callback. Here we'll want to style our elementsToExpand
with the animation that they should have like so:
let expansionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.intersectionRatio > 0) { entry.target.style.animation = `expansion 2s ease-in-out` } else { entry.target.style.animation = "none" } }) })
So, this is the whole behaviour that you have to recreate and now all we have to do is to define in our CSS this expansion
animation:
@keyframes expansion { from { transform: scaleY(0.1); } to { transform: scaleY(1); } }
And we're done! You can now check the full example in this preview or you can play around with the CodeSandbox yourself! I've added an extra element with a different animation so you could get a full grip of what's happening! 😄
What did you think about the Intersection Observer API
? Will you give it a try on your next project?
Let me know what you thought about this post and feel free to follow me on Twitter 🤘