Intersection Observer API
IntersectionObserver watches when elements enter or leave the viewport — without expensive scroll event listeners. Far more performant and the modern way to handle visibility-based effects.
// Basic observer:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target); // stop after first trigger
}
});
}, {
threshold: 0.3, // trigger when 30% visible
rootMargin: "50px" // expand trigger zone by 50px
});
document.querySelectorAll(".animate-on-scroll").forEach(el => {
observer.observe(el);
});
// Lazy loading images:
// HTML: <img data-src="real.jpg" src="placeholder.jpg" loading="lazy">
const imgObserver = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute("data-src");
obs.unobserve(img);
});
}, { rootMargin: "200px" }); // load 200px before visible
document.querySelectorAll("img[data-src]").forEach(img => imgObserver.observe(img));
// Infinite scroll:
const sentinel = document.querySelector("#load-more-sentinel");
const scrollObserver = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) loadMoreItems();
});
scrollObserver.observe(sentinel);
⚡ Key Takeaways
- IntersectionObserver is far more performant than scroll event listeners
- threshold: 0-1, or array — when to trigger (0=any pixel, 1=fully visible)
- rootMargin: extend or shrink the trigger zone
- Always unobserve after one-time actions (lazy loading, animations)
- Native HTML loading="lazy" handles image lazy loading without JS
🎯 Practice Exercises
EXERCISE 1
Build a page with 20 cards. Each card slides in from below when it enters the viewport. Use IntersectionObserver and CSS transitions. Ensure only cards not yet seen animate.