The Intersection Observer (IO) detects when an element enters or leaves the viewport (or a parent element). It can be used to easily add animation on scroll without external libraries.
IO is asynchronous and much more performant than scroll listeners 👍.
Btw, if you learn better through videos, I highly suggest this youtube tutorial by Kewin Powell.
Here's a basic example of a fade in animation on scroll using the intersection observer.
In this example we fade in an image on scroll by adding the class fadeIn
to it when it enters the viewport. This is the js:
const img = document.querySelector("img")
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("fadeIn")
}
})
}
const options = {}
const myObserver = new IntersectionObserver(callback, options)
myObserver.observe(img)
Easy, right? Let's get started 😁!
Creating an intersection observer
First, we create an intersection observer by calling its constructor and passing it a callback function and an optional options object.
const myObserver = new IntersectionObserver(callback, options)
The options
options
is an object with 3 properties:
const options = {
root: null,
rootMargin: '0px',
threshold: 0
}
In my fade in example, I've returned an empty object {}
so the default options will apply. (Same with not return anything. )
-
root: default
null
. it's the viewport. Can be the document or an HTML element. If the root isnull
, defaults todocument
. - rootMargin: default 0px. defines the offsets of each side of the root's bounding box. In other words, positive values reduce the root bounding box and negative values increase it. Try scrolling the 3 boxes in this example.
Similar to CSS's margin syntax: "0px 5px 10px 15px" means top: 0px, right: 5px, bottom: 10px and left: 0px. Accepts px and % only. ⚠ 0 is not an accepted value, use 0px or 0% instead.
- threshold: default 0. The threshold is a number between 0 and 1.0. 0 meaning as soon as one pixel is visible, the callback will be run. 1.0 means every pixel needs to be visible before calling the callback. (⚠ If you set the threshold to 1 and the element is bigger than the root, the number won't reach 1 because there will be some parts invisible at all time.)
The callback
The callback function takes a list of entries and an intersection observer as parameter.
const callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
The observer can be used to dynamically add or remove elements to observe. More on it below.
The focus is on the list of entries. There is one entry object for each observed element. It's common practice to use forEach
to iterate.
Each entry has the following helpful properties:
-
entry.isIntersecting
returns a boolean. True means the element is currently intersecting with the root. -
entry.target
returns the observed element.
I've used them both in the fadeIn animation:
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("fadeIn")
}
})
}
-
entry.boundingClientRect
returns the bounds rectangle of the observed element. -
entry.intersectionRatio
returns a number between 0.0 and 1.0 which indicates how much of the observed element is actually visible within the root.
Etc. 😁 I've named the most important ones. You can find a list of all the entry properties here.
Select elements to be observed
To select an element to observe, we use the observe() method of our Intersection Observer.
myObserver.observe(img)
And that's it! Now myObserver
will detect when img
enters or leave the viewport and trigger the callback.
If you want to observe many elements, you have to add them one by one.
myObserver.observe(img1)
myObserver.observe(img2)
myObserver.observe(img3)
Or by giving them a common class and iterate with forEach
:
const imgList = document.querySelectorAll(".imgToAnimate")
// setting your observer here
imgList.forEach(img => {
myObserver.observe(img)
})
To stop observing, call unobserve()
on the element:
myObserver.unobserve(img)
To stop observing every element at once call disconnect()
:
myObserver.disconnect()
You can also use those methods in the callback:
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("fadeIn")
// stop observing this element
observer.unobserve(entry.target)
}
})
}
Edit: It's a good practice to unobserve an element after we are done playing with it.
That's it!
I hope you've enjoyed this short intro on Intersection Observer 😃.
Source: MDN
Beside animating on scroll, it can be used to improve render speed and First Contentful Paint with lazy loading of scripts and media.
Beyond the basics
Here are a few examples of scroll animations with IO. I'll try to write a blog on each when I find some time 😅.
Enter & Leave Anim
Scroll To Top
Update current tab on scroll
Top comments (5)
Awesome! I usually use intersection observer for lazy loading of images or to play/pause videos when they enter the viewport... But this helps me to get rid of jquery for tos "reveal animations"... One thing I could suggest is add cleanup, when the target enters the viewport, remove the current image from the list of observable elements.... Or once you finish your work, disconnect it, and when the array of observables is empty, the garbage collector will remove the IO... Great post! I will "borrow" this idea for sure
Glad u find it useful and thanks a ton for the feedback Epresas 😃!! Yep, u could also use AOS for those reveal animations if u don't feel like writing the js.
U're right, I didn't think about adding a cleanup. I should call unobserve or disconnect after the animation has been triggered 👌. Feel free to borrow and show me if u have better ideas😉.
This just helped me a lot for work I am doing for my job - thank you!
Great content❤
I played with IO and it is wholesome.
I don't have to use Jquery anymore😁Cause i hate using it.
but I have a problem , I can't use animation on this ,,like the animation we made using @keyframe .🤕
Thanks for your post.
It is so useful.