The Codex / JavaScript / Intersection Observer

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

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.