What is IntersectionObserver?

Web APIsJavaScript

The short answer

IntersectionObserver lets you watch when an element enters or leaves the viewport (or another scrolling container) without running code on every scroll event. You give it a callback and one or more target elements, and the browser notifies you asynchronously when a target's visibility crosses a threshold you define. Because the work happens off the main scroll path, it is far cheaper than a scroll listener that measures positions by hand.

How it works

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log(entry.target, 'is now visible');
}
});
});
const target = document.querySelector('.box');
observer.observe(target);

The callback receives an array of entries, one per observed element whose visibility changed. The useful fields are:

  • isIntersecting: whether the element is currently in view
  • intersectionRatio: how much of it is visible, from 0 to 1
  • target: the observed element itself

You can tune the behavior with options:

const observer = new IntersectionObserver(callback, {
root: null, // null means the viewport
rootMargin: '0px 0px 200px 0px', // grow the root box to fire earlier
threshold: [0, 0.5, 1], // fire at 0%, 50%, and 100% visible
});

Lazy loading images

The most common use is loading images only when they are about to scroll into view:

const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img); // stop watching once loaded
});
}
);
document
.querySelectorAll('img[data-src]')
.forEach((img) => {
imageObserver.observe(img);
});

Calling unobserve once the image loads keeps the observer from doing extra work.

Infinite scroll

Place a small sentinel element at the bottom of a list and load more items when it appears:

const sentinel = document.querySelector('.sentinel');
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMoreItems();
}
});
observer.observe(sentinel);

In React

Set up the observer in useEffect and disconnect it in the cleanup function so it does not leak across renders or unmounts:

function LazySection({ onVisible }) {
const ref = useRef(null);
useEffect(() => {
const node = ref.current;
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
onVisible();
observer.disconnect();
}
},
{ threshold: 0.1 }
);
observer.observe(node);
return () => observer.disconnect();
}, [onVisible]);
return <div ref={ref}>Loads when visible...</div>;
}

Interview Tip

Contrast IntersectionObserver with a scroll listener. A scroll handler fires constantly and forces you to call getBoundingClientRect, which triggers layout. IntersectionObserver runs asynchronously, batches its work, and never blocks scrolling. Mentioning that difference shows you understand why the API exists.

Why interviewers ask this

IntersectionObserver comes up in discussions about performance, lazy loading, infinite scroll, and analytics impression tracking. Interviewers want to see that you reach for a purpose built API instead of measuring scroll positions by hand, and that you clean up observers in frameworks like React. It is a practical tool that removes a common source of janky scrolling.