Often you need to know whether an element is visible: lazy-load an image, start an animation, load more items, mark a section as current. Doing that with scroll events is slow and awkward. IntersectionObserver does it efficiently.
const observer = new IntersectionObserver(callback, options);
observer.observe(element);
The browser calls callback when an observed element’s visibility changes.
A first example
Scroll the result below. Elements report when they enter the view.
<style>
body { margin: 0; }
.box { height: 120px; margin: 12px; background: #d0e8f2; display: grid; place-items: center; transition: background .3s; }
.box.seen { background: #fdebd9; }
</style>
<div class="box" data-n="1">Box 1</div>
<div class="box" data-n="2">Box 2</div>
<div class="box" data-n="3">Box 3</div>
<div class="box" data-n="4">Box 4</div>
<script>
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("seen");
console.log("Box", entry.target.dataset.n, "is visible");
observer.unobserve(entry.target); // only react once
}
}
}, { threshold: 0.5 });
document.querySelectorAll(".box").forEach(box => observer.observe(box));
</script>
The entry
| Property | Meaning |
|---|---|
entry.isIntersecting | is it (partly) in view? |
entry.intersectionRatio | how much, from 0 to 1 |
entry.target | the element |
entry.boundingClientRect | its position |
Options
| Option | Meaning |
|---|---|
root | the scroll container to watch (default: the viewport) |
rootMargin | grows or shrinks the root box, e.g. "200px" to trigger early |
threshold | how much must be visible: 0 to 1, or a list |
Use 1: lazy loading images
const imageObserver = new IntersectionObserver((entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const img = entry.target;
img.src = img.dataset.src; // start the real download now
obs.unobserve(img);
}
}, { rootMargin: "200px" });
document.querySelectorAll("img[data-src]").forEach(img => imageObserver.observe(img));
For ordinary images, the HTML attribute loading="lazy" does this natively. Use the observer when you need custom behaviour.
Use 2: infinite scroll
Observe a small “sentinel” element at the bottom of the list. When it becomes visible, load more items.
const sentinel = document.querySelector("#sentinel");
new IntersectionObserver(async ([entry]) => {
if (entry.isIntersecting) await loadMore();
}).observe(sentinel);
Use 3: highlight the current section
Observe each section, and mark the matching nav link when its section is in view.
Stopping
observer.unobserve(element) stops watching one element, and observer.disconnect() stops watching everything. Do this when you no longer need it.
Why not scroll events?
Scroll events fire constantly and run on the main thread. IntersectionObserver is asynchronous, designed for the job and much cheaper.