Skip to content
Home JavaScript IntersectionObserver API in JavaScript

IntersectionObserver API in JavaScript

Where developers are forged. · Structured learning · Free forever.
📍 Part of: DOM → Topic 7 of 9
How to use the IntersectionObserver API — lazy loading images, infinite scroll, scroll-triggered animations, and the options that control when callbacks fire.
⚙️ Intermediate — basic JavaScript knowledge assumed
In this tutorial, you'll learn
How to use the IntersectionObserver API — lazy loading images, infinite scroll, scroll-triggered animations, and the options that control when callbacks fire.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

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

Example · JAVASCRIPT
12345678910111213141516171819202122
// 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" />
▶ Output
Images load as they approach the viewport

threshold and rootMargin Options

Example · JAVASCRIPT
12345678910111213141516171819202122
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);
});
▶ Output
Callback fires when element crosses each threshold

Infinite Scroll

Example · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435
// 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);
}
▶ Output
Loads more content before user reaches the bottom

🎯 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.

🔥
Naren Founder & Author

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.

← PreviousMutationObserver APINext →Web APIs Overview
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged