Intersection Observer API Case Studies



Good day, friends!

Overview


The Intersection Observer API (IOA) allows an application to asynchronously monitor the intersection of an element (target) with its parent (root) or viewport (viewport). In other words, this API provides a call to a specific function every time the target element intersects with root or viewport.

Examples of using:

  • Lazy or lazy loading of images
  • endless page scrolling
  • receiving information about the visibility of advertising for the purpose of calculating the cost of impressions
  • starting a process or animation in the user's field of vision


To start working with IOA, you need to use the constructor to create an observer object with two parameters - a callback function and settings:

// 
let options = {
    root: document.querySelector('.scroll-list'),
    rootMargin: '5px',
    threshold: 0.5
}

//   
let callback = function(entries, observer){
    ...
}

// 
let observer = new IntersectionObserver(callback, options)

Settings:

  • root - an element that acts as a viewport for target (ancestor of the target element or null for viewport)
  • rootMargin - indents around root (margin in CSS, by default all indents are 0)
  • threshold - a number or an array of numbers indicating the acceptable percentage of intersection of target and root

Next, the target element is created, which the observer watches:

let target = document.querySelector('.list-item')
observer.observe(target)

The callback call returns an object containing records of changes that have occurred with the target element:

let callback = (entries, observer) => {
    entries.forEach(entry => {
        // entry () - 
        //   entry.boundingClientRect
        //   entry.intersectionRatio
        //   entry.intersectionRect
        //   entry.isIntersecting
        //   entry.rootBounds
        //   entry.target
        //   entry.time
    })
}

The network is full of information on theory, but quite a bit of material on the practice of using IOA. I decided to fill this gap a little.

Examples


Lazy (delayed) image loading


Task: upload (show) images as the user scrolls the page.

The code:

//    
window.onload = () => {
    //  
    const options = {
        //    -  
        root: null,
        //  
        rootMargin: '0px',
        //   -  
        threshold: 0.5
    }

    //  
    const observer = new IntersectionObserver((entries, observer) => {
        //   - 
        entries.forEach(entry => {
            //    
            if (entry.isIntersecting) {
                const lazyImg = entry.target
                //     -   
                console.log(lazyImg)
                //   
                lazyImg.style.background = 'deepskyblue'
                //  
                observer.unobserve(lazyImg)
            }
        })
    }, options)

    //       img  
    const arr = document.querySelectorAll('img')
    arr.forEach(i => {
        observer.observe(i)
    })
}

Result: The


background of the container outside the viewport is white.



When you cross the viewing area by half, the background changes to sky blue.

β†’ Codepen

β†’  Github

Image Replacement


Task: change the placeholder image to the original when the user scrolls the page.

The code:

window.onload = () => {
    const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                console.log(entry)
                //        "data-src"
                entry.target.src = entry.target.dataset.src
                observer.unobserve(entry.target)
            }
        })
    }, { threshold: 0.5 })

    document.querySelectorAll('img').forEach(img => observer.observe(img))
}

Result:


The first image is uploaded because it is in the viewing area. The second is placeholder.



As you scroll further, the placeholder is replaced with the original image.

β†’ Codepen

β†’  Github

Change container background


Task: to change the background of the container when the user scrolls the page there and back.

The code:

window.addEventListener('load', event => {
    let box = document.querySelector('div')
    // ratio -   
    let prevRatio = 0.0

    let observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            let curRatio = entry.intersectionRatio
            
            //      -  -
            //           (  )
           //      
            curRatio > prevRatio ? entry.target.style.background = `rgba(40,40,190,${curRatio})` : entry.target.style.background = `rgba(190,40,40,${curRatio})`

            prevRatio = curRatio
        })
    }, {
        threshold: buildThresholdList()
    })

    observer.observe(box)
    
    //    
    //      20 ,   
    function buildThresholdList() {
        let thresholds = []
        let steps = 20

        for (let i = 1.0; i <= steps; i++) {
            let ratio = i / steps
            thresholds.push(ratio)
        }
        return thresholds
    }
})

Result: The


background of the container changes from light blue ...



through blue ...



to light red.

β†’ Codepen

β†’  Github

Work with video


Task: Pause the running video and start it again depending on the video falling into the viewing area.

The code:

window.onload = () => {
    let video = document.querySelector('video')

    let observer = new IntersectionObserver(() => {
        //   
        if (!video.paused) {
            //  
            video.pause()
        //      (   > 0)
        } else if(video.currentTime != 0) {
            //  
            video.play()
        }
    }, { threshold: 0.4 })

    observer.observe(video)
}

Result:



While the video is in the viewing area, it plays.



As soon as the video goes beyond the viewing area by more than 40%, its playback pauses. If you hit the viewing area> 40% of the video, its playback resumes.

β†’ Codepen

β†’  Github

Page View Progress


Task: show the progress of viewing the page as the user scrolls the page.

The code:

//          
let p = document.querySelector('p')
// n -   
let n = 0

let observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if(entry.isIntersecting){
            // observer   div
            //       
            //     
            p.textContent = `${n++} div viewed`
            observer.unobserve(entry.target)
        }
    })
}, {threshold: 0.9})

document.querySelectorAll('div').forEach(div => observer.observe(div))

Result: The



page just loaded, so we have not yet looked at any containers.



When the end of the page is reached, the paragraph displays information about viewing 4 divs.

β†’ Codepen

β†’  Github

Endless scrolling


Task: implement an endless list.

The code:

let ul = document.querySelector('ul')
let n = 1

//    
function createLi(){
    li = document.createElement('li')
    li.innerHTML = `${++n} item`
    ul.append(li)
}

//  ,        
//     
//         li
//       () 
let observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            createLi()
        }
        observer.unobserve(entry.target)
        observer.observe(document.querySelector('li:last-child'))
    })
}, {
    threshold: 1
})

observer.observe(document.querySelector('li'))

Result:



We have 12 list items. The last item is outside the viewport.



When you try to get to the last element, a new (last) element is created that is hidden from the user. And so on to infinity.

β†’ Codepen

β†’  Github

Resizing a child when resizing a parent


Task: establish the dependence of the size of one element on another.

The code:

//      -   
//      
let info = document.querySelector('.info')
let parent = document.querySelector('.parent')
let child = document.querySelector('.child')
//     50px  
child.style.width = parent.offsetWidth - 50 + 'px'
//     
info.textContent = `child width: ${child.offsetWidth}px`

let options = {
    //      
    root: parent,
    threshold: 1
}

let observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        //         50px
        if ((entry.target.parentElement.offsetWidth - entry.target.offsetWidth) < 50) {
            //     50px
            entry.target.style.width = entry.target.offsetWidth - 50 + 'px'
        }
    })
}, options)

observer.observe(child)

//  ,  ,       IOA
//       resize
window.addEventListener('resize', () => {
    info.textContent = `child width: ${child.offsetWidth}px`
    if ((parent.offsetWidth - child.offsetWidth) > 51) {
        child.style.width = child.offsetWidth + 50 + 'px'
    }
})

Result:



Initial state.



When reducing the width of the parent element, the width of the child element decreases. At the same time, the distance between them almost always equals 50px (β€œalmost” is due to the implementation of the inverse mechanism).

β†’ Codepen

β†’  Github

Work with animation


Task: animate an object when it is visible.

The code:

//        
//     
//  - 
let observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        entry.isIntersecting ? entry.target.classList.replace('to-left', 'to-right') : entry.target.classList.replace('to-right', 'to-left')
    })
}, {
    threshold: .5
})

observer.observe(document.querySelector('img'))

Result:



We see part of Bart's head. Bart pressed to the left side of the viewing area.



If more than 50% of Bart falls into the viewing area, he moves to the middle. When more than 50% of Bart leaves the viewing area, it returns to its initial position.

β†’ Codepen

β†’  Github

Thank you for your attention.

All Articles