Intersection Observer
Efficiently detect when elements enter or leave the viewport — lazy loading images, scroll animations, infinite scroll, and sticky headers.
What it is, in plain English: The Intersection Observer API lets you observe when one element intersects with another (or with the viewport). Instead of listening to scroll events and measuring element positions (slow and janky), you declare what you want to watch and a callback fires only when visibility changes. This is how lazy loading, scroll-triggered animations, and infinite scroll are built in modern JavaScript.
Watch elements — without scroll events
The old way to detect if something is visible — listening to the scroll event
and calling getBoundingClientRect() every pixel — fires hundreds of times
per second and forces layout recalculation each time. Intersection Observer does the
same thing, runs off the main thread, and fires only when visibility actually changes.
Basic usage
// Create an observer with a callback and options:
const observer = new IntersectionObserver((entries) => {
// 'entries' = array of elements whose visibility changed
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(entry.target, "is now visible!");
} else {
console.log(entry.target, "is now hidden");
}
});
}, {
threshold: 0.1, // fire when 10% of element is visible
});
// Watch elements:
const section = document.querySelector("#hero");
observer.observe(section);
// Stop watching:
observer.unobserve(section);
// Stop all observations:
observer.disconnect();Lazy loading images (the main use case)
<!-- HTML: use data-src instead of src (placeholder prevents layout shift): -->
<img class="lazy" data-src="/real-image.jpg" src="/placeholder.jpg" alt="...">// JavaScript: load the real image when it's about to enter the viewport:
const imgObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return; // skip if not visible
const img = entry.target;
img.src = img.dataset.src; // swap to real URL
img.classList.remove("lazy");
imgObserver.unobserve(img); // done — stop watching this image
});
}, {
rootMargin: "200px 0px", // start loading 200px BEFORE the image enters viewport
});
// Watch all lazy images:
document.querySelectorAll("img.lazy").forEach(img => imgObserver.observe(img));Scroll animations
/* CSS: elements start invisible */
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}// JavaScript: add class when element enters viewport
const animObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
animObserver.unobserve(entry.target); // animate only once
}
});
}, { threshold: 0.15 });
document.querySelectorAll(".fade-in").forEach(el => animObserver.observe(el));
Try It Yourself
Official Sources
- MDN Web Docs — Intersection Observer API
- W3C Intersection Observer Spec
- MDN Web Docs — ResizeObserver
- web.dev — Lazy loading images
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.