🚀AdvancedBrowser Observers

Intersection Observer API

Efficiently detect when elements enter or leave the viewport using IntersectionObserver enabling lazy loading, infinite scroll, and scroll animations.

Overview

The Intersection Observer API provides an efficient way to detect when DOM elements enter or leave the viewport (or another element). It replaces the pattern of checking `getBoundingClientRect()` on every scroll event which causes layout thrashing with an asynchronous, browser-optimized callback. The observer fires its callback with an array of `IntersectionObserverEntry` objects, each describing an observed element's intersection state: `isIntersecting`, `intersectionRatio`, `boundingClientRect`, and `intersectionRect`. The `threshold` option controls at what visibility percentage the callback fires (0 = any pixel visible, 1 = fully visible). Common use cases: lazy loading images, infinite scroll, read-progress tracking, animating elements when they scroll into view, and pausing off-screen videos.

Key Points

  • •`new IntersectionObserver(callback, options)` creates an observer; `observe(el)` starts tracking an element
  • •`entry.isIntersecting` true when element has entered the viewport (or root)
  • •`entry.intersectionRatio` number 0–1 representing what fraction of the element is visible
  • •`threshold` option array of ratios (e.g., `[0, 0.5, 1]`) at which the callback fires
  • •`rootMargin` option expand/contract the root bounds (like CSS margin), e.g., `"200px 0px"` to load 200px before entering viewport
  • •`root` option use a scrollable container instead of the viewport
  • •`observer.unobserve(el)` stop observing a single element; `observer.disconnect()` stop all

Examples

Lazy loading images when they enter the viewport

<img
  class="lazy"
  data-src="/images/photo1.webp"
  src="/images/placeholder.svg"
  alt="Gallery photo"
  width="800"
  height="600"
>

<script>
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      imageObserver.unobserve(img);
    }
  });
}, {
  rootMargin: '200px 0px',
  threshold: 0
});

document.querySelectorAll('img.lazy').forEach(img => imageObserver.observe(img));
</script>

rootMargin: "200px 0px" triggers loading before the image reaches the viewport no visible pop-in.

Animating elements when they scroll into view

<style>
  .reveal {
    opacity: 0;
    transform: translateY(30px);
    transition: opacity 0.5s ease, transform 0.5s ease;
  }
  .reveal.visible {
    opacity: 1;
    transform: translateY(0);
  }
  @media (prefers-reduced-motion: reduce) {
    .reveal { opacity: 1; transform: none; transition: none; }
  }
</style>

<div class="reveal"><h2>Feature 1</h2></div>
<div class="reveal"><h2>Feature 2</h2></div>

<script>
const revealObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
        revealObserver.unobserve(entry.target);
      }
    });
  },
  { threshold: 0.15 }
);

document.querySelectorAll('.reveal').forEach(el => revealObserver.observe(el));
</script>

Always add prefers-reduced-motion support for accessibility.

Infinite scroll sentinel

<ul id="feed"></ul>
<div id="sentinel"></div>

<script>
let page = 1;
let loading = false;

const sentinel = document.getElementById('sentinel');
const feedObserver = new IntersectionObserver(async (entries) => {
  if (entries[0].isIntersecting && !loading) {
    loading = true;
    page++;
    const items = await fetchPage(page);
    renderItems(items);
    loading = false;
    if (items.length === 0) feedObserver.unobserve(sentinel);
  }
}, { threshold: 1.0 });

feedObserver.observe(sentinel);
</script>

The sentinel element triggers the next page load when fully visible.

Common Mistakes

  • Not calling `unobserve(el)` after a one-time action (e.g., load an image) the observer keeps firing on every viewport change
  • Using threshold: 1.0 for lazy loading images larger than the viewport will never fully intersect; use threshold: 0
  • Forgetting `prefers-reduced-motion` when using scroll animations respect users who experience motion sickness

Note

IntersectionObserver reports geometric intersection fast scroll or tab switching can trigger callbacks even if the user never visually saw the element.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What does entry.isIntersecting mean in an IntersectionObserver callback?