DEV Community

Cover image for Introduction to scroll animations with Intersection Observer
ljc-dev
ljc-dev

Posted on • Edited on

Introduction to scroll animations with Intersection Observer

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The options

options is an object with 3 properties:

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 0
}
Enter fullscreen mode Exit fullscreen mode

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 is null, defaults to document.
  • 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
  });
};
Enter fullscreen mode Exit fullscreen mode

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")
    }
  })
}
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

To stop observing, call unobserve() on the element:

myObserver.unobserve(img)
Enter fullscreen mode Exit fullscreen mode

To stop observing every element at once call disconnect():

myObserver.disconnect()
Enter fullscreen mode Exit fullscreen mode

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)
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

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

And more to come 😁!

Top comments (5)

Collapse
 
epresas profile image
epresas

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

Collapse
 
ljcdev profile image
ljc-dev

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

Collapse
 
maxharrisnet profile image
Max Harris

This just helped me a lot for work I am doing for my job - thank you!

Collapse
 
mdsourav76046 profile image
Md.Sourav • Edited

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

Collapse
 
ahmadullahnikzad profile image
ahmadullah

Thanks for your post.
It is so useful.