Skip to main content

IntersectionObserver

IntersectionObserver is a built-in browser API that lets you track when an element enters or leaves the viewport, without heavy scroll handlers. It is an object that "watches" an element and reports when it intersects the viewport or another container.

Theory

TL;DR

  • IntersectionObserver tracks the intersection of an element with the viewport or a given container.
  • It replaces scroll events, manual getBoundingClientRect() checks and window.onscroll.
  • Creation: new IntersectionObserver(callback, options) plus observer.observe(element).
  • Options: root, rootMargin, threshold.
  • The callback receives an array of IntersectionObserverEntry objects with isIntersecting, intersectionRatio, target.
  • Stopping: observer.unobserve(target) for one element, observer.disconnect() for all of them.

Quick example

javascript
const target = document.querySelector('#target'); const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { console.log('The element entered the viewport!'); } else { console.log('The element left the viewport!'); } }); }); observer.observe(target); // start observing

A simple example on a page

html
<div style="height: 120vh;"></div> <div id="target" style="height: 100px; background: coral;">I am the target</div> <div style="height: 120vh;"></div> <script> const target = document.querySelector('#target'); const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { console.log('The element entered the viewport!'); } else { console.log('The element left the viewport!'); } }); }); observer.observe(target); // start observing </script>

When you scroll the page and the block reaches the screen, the console prints:

text
The element entered the viewport!

This is an excellent alternative to:

  • scroll events;
  • manual checks with getBoundingClientRect();
  • window.onscroll handlers, which put a heavy load on the page.

Constructor parameters

javascript
new IntersectionObserver(callback, options);
ParameterWhat it does
callbackThe function called on every intersection change
options.rootThe container to track inside (the browser window by default)
options.rootMarginOffsets (for example '0px 0px -100px 0px'), like CSS margin
options.thresholdThe trigger threshold, the visible fraction of the element (from 0 to 1)

An example with options:

javascript
const observer = new IntersectionObserver(callback, { root: null, // the browser window rootMargin: '0px 0px -100px 0px', // fires a little earlier threshold: 0.5 // when 50% of the element is visible });

The entry object in the callback

Every element the observer watches is represented by an IntersectionObserverEntry object:

javascript
entry.isIntersecting; // true / false entry.intersectionRatio; // the visible fraction (from 0 to 1) entry.boundingClientRect; // the element's position entry.rootBounds; // the bounds of the root area entry.target; // the element itself

The callback receives an array of such entries because one observer can watch hundreds of elements, and the browser delivers all the changes of a tick together.

Practical examples

Elements fading in lazily:

javascript
const items = document.querySelectorAll('.fade'); const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }); items.forEach((item) => observer.observe(item));
css
.fade { opacity: 0; transform: translateY(20px); transition: 0.5s; } .fade.visible { opacity: 1; transform: translateY(0); }

The elements float in smoothly as they reach the screen.

Lazy loading of images:

html
<img data-src="photo.jpg" alt="photo" width="400" height="300" />
javascript
const images = document.querySelectorAll('img[data-src]'); const observer = new IntersectionObserver((entries, obs) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; obs.unobserve(img); // stop watching this image } }); }); images.forEach((img) => observer.observe(img));

Now images load only when they become visible, which saves bandwidth and speeds up the site.

Advantages over scroll, and controlling the observer

A plain scroll handlerIntersectionObserver
Fires on every pixel of scrollingWorks asynchronously and efficiently
You compute getBoundingClientRect() by handIt does the maths for you
CPU loadAlmost no load
Awkward with many elementsOne observer watches hundreds of elements

To stop watching one particular element:

javascript
observer.unobserve(target);

To shut the observer down completely:

javascript
observer.disconnect();

Summary:

What it doesIntersectionObserver
TracksWhen an element enters or leaves the viewport
Replacesonscroll and getBoundingClientRect()
Used forLazy loading, scroll animations, view analytics
PerformanceVery high, the browser does the maths, not your JS code

Just remember: IntersectionObserver is a "visibility watcher". It tells you when an element appeared on screen, and it does so efficiently, without extra scroll events.

Common mistakes

  • Forgetting to call observer.observe(element). An observer on its own watches nothing.
  • Expecting the callback only when the element appears. It also fires when the element leaves, so the entry.isIntersecting check is mandatory.
  • Not calling unobserve() after a one-off action (loading an image, say) and redoing the work on every intersection.
  • Confusing threshold: 0 with threshold: 1: the first fires on the element's first pixel, the second only when the whole element is visible.
  • Writing rootMargin in percentages where pixels are needed, or without units at all. A unitless value is rejected by the browser.
  • Passing a root that is not an ancestor of the target. Observation then does not behave as expected.
  • Not shutting the observer down when the component is destroyed (disconnect()), which leaks memory and keeps callbacks for removed nodes.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.