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
IntersectionObservertracks the intersection of an element with the viewport or a given container.- It replaces
scrollevents, manualgetBoundingClientRect()checks andwindow.onscroll. - Creation:
new IntersectionObserver(callback, options)plusobserver.observe(element). - Options:
root,rootMargin,threshold. - The callback receives an array of
IntersectionObserverEntryobjects withisIntersecting,intersectionRatio,target. - Stopping:
observer.unobserve(target)for one element,observer.disconnect()for all of them.
Quick example
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 observingA simple example on a page
<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:
The element entered the viewport!This is an excellent alternative to:
scrollevents;- manual checks with
getBoundingClientRect(); window.onscrollhandlers, which put a heavy load on the page.
Constructor parameters
new IntersectionObserver(callback, options);| Parameter | What it does |
|---|---|
callback | The function called on every intersection change |
options.root | The container to track inside (the browser window by default) |
options.rootMargin | Offsets (for example '0px 0px -100px 0px'), like CSS margin |
options.threshold | The trigger threshold, the visible fraction of the element (from 0 to 1) |
An example with options:
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:
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 itselfThe 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:
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));.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:
<img data-src="photo.jpg" alt="photo" width="400" height="300" />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 handler | IntersectionObserver |
|---|---|
| Fires on every pixel of scrolling | Works asynchronously and efficiently |
You compute getBoundingClientRect() by hand | It does the maths for you |
| CPU load | Almost no load |
| Awkward with many elements | One observer watches hundreds of elements |
To stop watching one particular element:
observer.unobserve(target);To shut the observer down completely:
observer.disconnect();Summary:
| What it does | IntersectionObserver |
|---|---|
| Tracks | When an element enters or leaves the viewport |
| Replaces | onscroll and getBoundingClientRect() |
| Used for | Lazy loading, scroll animations, view analytics |
| Performance | Very high, the browser does the maths, not your JS code |
Just remember:
IntersectionObserveris a "visibility watcher". It tells you when an element appeared on screen, and it does so efficiently, without extrascrollevents.
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.isIntersectingcheck is mandatory. - Not calling
unobserve()after a one-off action (loading an image, say) and redoing the work on every intersection. - Confusing
threshold: 0withthreshold: 1: the first fires on the element's first pixel, the second only when the whole element is visible. - Writing
rootMarginin percentages where pixels are needed, or without units at all. A unitless value is rejected by the browser. - Passing a
rootthat 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 readyA concise answer to help you respond confidently on this topic during an interview.