IntersectionObserver
Watches whether an element is visible within the window or a scrollable container, and calls a function whenever that changes.
- Returns
- A new
IntersectionObserverobject.
Example
#const feed = document.querySelector('#feed')
const lessons = feed.querySelectorAll('p')
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const state = entry.isIntersecting ? 'visible' : 'hidden'
console.log(`${entry.target.textContent}: ${state}`)
}
}, { root: feed })
lessons.forEach((lesson) => observer.observe(lesson))
console.log(`Observing ${lessons.length} lessons`)The first entry comes from synchronous code, and the lesson states arrive later, in the callback. “Loops” is only partly visible, but with the default threshold: 0 even a single pixel counts. Scroll the list in the preview, and the console will show further changes.
Definition and usage
#IntersectionObserver tells you when an element enters or leaves the visible area. Instead of checking elements’ positions on every scroll, you create an observer with a callback function, and the browser calls it at the right moment on its own. This underlies lazy loading, infinite lists, scroll-triggered animations, and view counting.
The callback receives an array of IntersectionObserverEntry objects. The most important fields are target (the observed element), isIntersecting (whether the element is visible), and intersectionRatio (what fraction of the element is visible, from 0 to 1). The first call happens right after observe(), so you immediately learn the starting state.
You pass options as the constructor’s second argument. root is the element visibility is measured against (the browser window by default). rootMargin expands or shrinks that area, e.g. "200px" lets you start loading before the element appears on screen. threshold sets at what visible fraction to call the function, e.g. 0.5 or [0, 0.5, 1].
The observe() method adds an element to the observation, unobserve() removes it, and disconnect() ends the observation entirely. The callback runs asynchronously, so its console entries appear after synchronous code.
Syntax
#new IntersectionObserver(callback)
new IntersectionObserver(callback, options)
observer.observe(element)
observer.unobserve(element)
observer.disconnect()Parameters
#callback
function (entries, observer)
The function called when visibility changes. It receives an array of entries and the observer itself.options
optional object
root(an element, ornullfor the window),rootMargin(a margin written like in CSS, e.g."0px 0px 200px 0px"), andthreshold(a number or an array of numbers from 0 to 1).
More examples
#const viewport = document.querySelector('#viewport')
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const percent = Math.round(entry.intersectionRatio * 100)
console.log(`${entry.target.textContent}: ${percent}% visible`)
}
}, { root: viewport, threshold: [0, 0.25, 0.5, 0.75, 1] })
viewport.querySelectorAll('.card').forEach((card) => observer.observe(card))The second card sticks out past the container, so only a quarter of it is visible. Scroll the container in the preview, and the observer will fire again each time the visible fraction crosses one of the threshold values.
const list = document.querySelector('#list')
const sentinel = document.querySelector('#sentinel')
let page = 0
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return
page += 1
for (let i = 1; i <= 5; i++) {
const item = document.createElement('li')
item.textContent = `Task ${(page - 1) * 5 + i}`
sentinel.before(item)
}
console.log(`Loaded page ${page}`)
if (page === 3) {
observer.disconnect()
sentinel.textContent = 'These are all the tasks'
}
}, { root: list })
observer.observe(sentinel)A sentinel element sits at the end of the list. When it becomes visible, we add more tasks right before it. Scroll the list to the bottom in the preview to load further pages. After the third one, the observer disconnects.
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
}, { rootMargin: '200px' })
document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img))Without the root option, visibility is measured against the browser window. An image only gets its real address once it’s 200px from the screen, and then stops being observed. For simple cases, the HTML attribute loading="lazy" achieves the same thing.
Browser support
#Widely available · since 2019
Works in all modern browsers, phones included. You can use it without worry.
- Chrome
- Edge
- Firefox
- Safari
Good practices
#- After a one-time action, such as loading an image, call
observer.unobserve(element). The observer won’t keep tracking the element for no reason. - A single observer can track many elements. Don’t create a separate observer for every item in a list.
- For simple image lazy loading, the
loading="lazy"HTML attribute is often enough. Use an observer when you need your own logic.
Related entries
#- ResizeObserverCalls a function every time an observed element’s size changes, no matter the cause of the change.
- MutationObserverWatches for changes in the DOM tree: added and removed elements, attribute and text changes, and reports them in a single function.
- Element.getBoundingClientRect()Returns an element’s size and its position relative to the viewport, as a DOMRect object.
- Element.scrollIntoView()Scrolls the page and any scrollable containers so the element appears on screen, either instantly or smoothly.
- HTMLElement.datasetAn object with an element’s data-* attribute values, for reading and storing your own data.
Spotted a mistake or missing an example? Write to us.