Web API · Scroll instrumentation

IntersectionObserver, measured live

Four snippets from the article, wired up on this page. Every callback also logs to the panel at the bottom-right — and to your real DevTools console.

↓ Scroll — the page is the demo

Snippet 01 — Basic observation

Is the element in the viewport?

The default observer fires once when the target crosses in or out of view. Watch isIntersecting flip as the dashed box enters and leaves your screen.

const target = document.querySelector('#watched-box');
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    console.log(`Element is intersecting: ${entry.isIntersecting}`);
    console.log(`Visible ratio: ${entry.intersectionRatio.toFixed(2)}`);
  });
});
observer.observe(target);
#watched-box — scroll me in and out
keep scrolling — the next images haven't loaded yet

Snippet 02 — Lazy loading

Load images only when needed

Each <img> starts as a placeholder with a data-src. When it intersects, the real source is swapped in and the observer stops watching it with unobserve.

const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, obs) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      console.log(`Loaded image: ${img.src}`);
      obs.unobserve(img);
    }
  });
});
images.forEach((img) => imageObserver.observe(img));
Lazy image 1
img[data-src] · 01
Lazy image 2
img[data-src] · 02
Lazy image 3
img[data-src] · 03
next: a card whose opacity is the intersection ratio

Snippet 03 — Thresholds & rootMargin

Drive styles from the ratio

With five thresholds and a bottom rootMargin of −10%, the callback fires at each visibility step. The card's opacity is entry.intersectionRatio — scroll slowly and watch it fade in.

const card = document.querySelector('.fade-card');
const ratioObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      card.style.opacity = entry.intersectionRatio.toFixed(2);
      console.log(`Ratio: ${entry.intersectionRatio.toFixed(2)}`);
    });
  },
  { threshold: [0, 0.25, 0.5, 0.75, 1], rootMargin: '0px 0px -10% 0px' }
);
ratioObserver.observe(card);

.fade-card

My opacity equals my intersection ratio:

0.00

last one: observing a feed, then cleaning up

Snippet 04 — Cleanup

Disconnect when you're done

The observer watches the feed container. The article snippet disconnects on beforeunload; the button below triggers the same cleanup so you can see it happen. After disconnecting, scrolling the feed in and out logs nothing.

const list = document.querySelector('#feed');
const feedObserver = new IntersectionObserver((entries) => {
  console.log(`${entries.length} entries changed`);
});
feedObserver.observe(list);
window.addEventListener('beforeunload', () => {
  feedObserver.disconnect();
  console.log('Observer disconnected');
});