Skip to content

JavaScript Tutorial Web APIs

IntersectionObserver

Lesson 211 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

Often you need to know whether an element is visible: lazy-load an image, start an animation, load more items, mark a section as current. Doing that with scroll events is slow and awkward. IntersectionObserver does it efficiently.

script.js
JS
const observer = new IntersectionObserver(callback, options);
observer.observe(element);

The browser calls callback when an observed element’s visibility changes.

A first example

Scroll the result below. Elements report when they enter the view.

index.html
HTML
<style>
  body { margin: 0; }
  .box { height: 120px; margin: 12px; background: #d0e8f2; display: grid; place-items: center; transition: background .3s; }
  .box.seen { background: #fdebd9; }
</style>
<div class="box" data-n="1">Box 1</div>
<div class="box" data-n="2">Box 2</div>
<div class="box" data-n="3">Box 3</div>
<div class="box" data-n="4">Box 4</div>
<script>
  const observer = new IntersectionObserver(entries => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        entry.target.classList.add("seen");
        console.log("Box", entry.target.dataset.n, "is visible");
        observer.unobserve(entry.target);          // only react once
      }
    }
  }, { threshold: 0.5 });

  document.querySelectorAll(".box").forEach(box => observer.observe(box));
</script>

The entry

PropertyMeaning
entry.isIntersectingis it (partly) in view?
entry.intersectionRatiohow much, from 0 to 1
entry.targetthe element
entry.boundingClientRectits position

Options

OptionMeaning
rootthe scroll container to watch (default: the viewport)
rootMargingrows or shrinks the root box, e.g. "200px" to trigger early
thresholdhow much must be visible: 0 to 1, or a list

Use 1: lazy loading images

script.js
JS
const imageObserver = new IntersectionObserver((entries, obs) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;
    const img = entry.target;
    img.src = img.dataset.src;       // start the real download now
    obs.unobserve(img);
  }
}, { rootMargin: "200px" });

document.querySelectorAll("img[data-src]").forEach(img => imageObserver.observe(img));

For ordinary images, the HTML attribute loading="lazy" does this natively. Use the observer when you need custom behaviour.

Use 2: infinite scroll

Observe a small “sentinel” element at the bottom of the list. When it becomes visible, load more items.

script.js
JS
const sentinel = document.querySelector("#sentinel");
new IntersectionObserver(async ([entry]) => {
  if (entry.isIntersecting) await loadMore();
}).observe(sentinel);

Use 3: highlight the current section

Observe each section, and mark the matching nav link when its section is in view.

Stopping

observer.unobserve(element) stops watching one element, and observer.disconnect() stops watching everything. Do this when you no longer need it.

Why not scroll events?

Scroll events fire constantly and run on the main thread. IntersectionObserver is asynchronous, designed for the job and much cheaper.

info
Accessibility
Do not hide important content until it is scrolled into view, and respect reduced motion for animations that start on visibility: Reduced Motion.