IntersectionObserver API in JavaScript
- IntersectionObserver is the right tool for lazy loading, scroll animations, and infinite scroll.
- threshold: 0 fires when any part of the element is visible. threshold: 1 fires when the entire element is visible.
- rootMargin extends the effective viewport — positive values fire earlier, negative values later.
IntersectionObserver asynchronously notifies you when a target element enters or exits the viewport (or another container). Create an observer with a callback and options, then call observer.observe(element). The callback receives an array of IntersectionObserverEntry objects with isIntersecting, intersectionRatio, and the target element.
Basic Usage — Lazy Loading Images
// Lazy load images as they scroll into view const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; // load the real image img.classList.add('loaded'); observer.unobserve(img); // stop watching once loaded } }); }, { rootMargin: '200px', // start loading 200px before entering viewport threshold: 0 // fire as soon as any part is visible }); // Observe all images with data-src attribute document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); // HTML: // <img data-src="/images/photo.jpg" src="/images/placeholder.jpg" />
threshold and rootMargin Options
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { console.log(`${entry.target.id}: ${entry.intersectionRatio.toFixed(2)} visible`); console.log('isIntersecting:', entry.isIntersecting); }); }, { root: null, // null = viewport (default) rootMargin: '0px', // expand/contract the root's bounding box threshold: [0, 0.25, 0.5, 0.75, 1.0] // fire callback at each threshold // threshold: 0.5 fires when element is 50% visible }); // Scroll animation — add class when element enters viewport const animationObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { entry.target.classList.toggle('visible', entry.isIntersecting); }); }, { threshold: 0.1 }); // fire when 10% visible document.querySelectorAll('.animate-on-scroll').forEach(el => { animationObserver.observe(el); });
Infinite Scroll
// Observe a sentinel element at the bottom of the list const sentinel = document.querySelector('#load-more-sentinel'); let page = 1; let isLoading = false; const scrollObserver = new IntersectionObserver(async (entries) => { const [entry] = entries; if (!entry.isIntersecting || isLoading) return; isLoading = true; page++; try { const response = await fetch(`/api/items?page=${page}`); const items = await response.json(); if (items.length === 0) { scrollObserver.unobserve(sentinel); // no more pages sentinel.remove(); return; } items.forEach(item => renderItem(item)); } finally { isLoading = false; } }, { rootMargin: '300px' }); // load before hitting the bottom scrollObserver.observe(sentinel); function renderItem(item) { const el = document.createElement('div'); el.textContent = item.title; document.querySelector('#list').appendChild(el); }
🎯 Key Takeaways
- IntersectionObserver is the right tool for lazy loading, scroll animations, and infinite scroll.
- threshold: 0 fires when any part of the element is visible. threshold: 1 fires when the entire element is visible.
- rootMargin extends the effective viewport — positive values fire earlier, negative values later.
- Always call observer.unobserve(element) after the element is done being observed to avoid memory leaks.
- The callback receives an array of entries — multiple elements may change visibility state at the same time.
Interview Questions on This Topic
- QHow would you implement lazy loading of images without IntersectionObserver?
- QWhat is the threshold option in IntersectionObserver?
- QHow would you implement infinite scroll using IntersectionObserver?
Frequently Asked Questions
What is the difference between IntersectionObserver and scroll event listeners?
Scroll events fire on every scroll frame and require synchronous getBoundingClientRect() calls which trigger layout. IntersectionObserver runs asynchronously off the main thread, only calls back when visibility actually changes, and is significantly more performant. Use IntersectionObserver for any visibility-based logic.
What does rootMargin: '200px' mean?
It expands the root element's bounding box by 200px in all directions for the purpose of intersection calculations. A target 200px below the viewport will be considered 'intersecting' with a rootMargin of '200px 0px'. This is how you start loading content before the user scrolls to it.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.