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 demoSnippet 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);
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));
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
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');
});
- 09:12feedObserver is watching this whole list as one target.
- 09:15Scroll it out of view and back — each crossing logs an entry.
- 09:21One observer, one target: entries.length is 1 per change.
- 09:30Press the button to disconnect early.