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 => {
})
}
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)
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β GithubImage 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)
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β GithubChange 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')
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)
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β GithubWork 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()
} 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β GithubPage View Progress
Task: show the progress of viewing the page as the user scrolls the page.The code:
let p = document.querySelector('p')
let n = 0
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting){
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β GithubEndless 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)
}
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β GithubResizing 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')
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 => {
if ((entry.target.parentElement.offsetWidth - entry.target.offsetWidth) < 50) {
entry.target.style.width = entry.target.offsetWidth - 50 + 'px'
}
})
}, options)
observer.observe(child)
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β GithubWork 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β GithubThank you for your attention.