Skip to content

Intersection Observer

This lesson explains Intersection Observer with clear examples, use cases, and best practices so you can use HTML5 APIs confidently in real web applications.

HTML5 Intersection Observer API Overview

The HTML5 Intersection Observer API allows web applications to detect when an element enters or leaves the visible area of a page, scroll container, or viewport. It is a modern, efficient alternative to repeatedly checking element positions during scroll events.

Intersection Observer is commonly used for lazy loading images, infinite scrolling, scroll-based animations, advertisement visibility tracking, analytics impressions, active section highlighting, video autoplay when visible, and performance-friendly UI updates.

Feature Description
API Name Intersection Observer API
Main Object IntersectionObserver
Main Method observe()
Stop Method unobserve() or disconnect()
Best Use Detect element visibility without scroll performance issues.
Common Uses Lazy loading, infinite scroll, animations, tracking, navigation.

Basic Intersection Observer Example

// HTML5 Intersection Observer API

const target =
  document.querySelector("#target");

const observer =
  new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        console.log("Element is visible.");
      } else {
        console.log("Element is not visible.");
      }
    });
  });

observer.observe(target);

This example watches a target element and runs a callback whenever the element intersects with the viewport.

How Intersection Observer Works

The browser observes one or more target elements and calls your callback when their visibility changes relative to a root area.

Step Description Example Syntax
Create Observer Create a new observer with callback and options. new IntersectionObserver(callback, options)
Select Target Choose the element to observe. document.querySelector("#target")
Start Watching Observe the selected element. observer.observe(target)
Receive Entry Read visibility information from each entry. entry.isIntersecting
Stop Watching Stop observing one element or all elements. observer.unobserve(target)

Intersection Observer Options

const options = {
  root: null,
  rootMargin: "0px",
  threshold: 0.5
}

const observer =
  new IntersectionObserver(
    callback,
    options
  );
Option Description Example Value
root Element used as the viewing area. Use null for viewport. null
rootMargin Margin around the root before intersection is calculated. "100px 0px"
threshold Percentage of target visibility required to trigger callback. 0, 0.5, 1

IntersectionObserverEntry Properties

Property Description Common Use
isIntersecting Returns true when the target intersects the root. Check visible or hidden state.
intersectionRatio Percentage of target currently visible. Track visibility progress.
target The observed element. Add classes or load content.
boundingClientRect Target element dimensions and position. Debug layout visibility.
intersectionRect Visible portion of the target. Calculate visible area.
rootBounds Bounds of the root container. Compare target with viewport or container.
time Timestamp when intersection was recorded. Analytics and timing logic.

Lazy Load Images Example

<img
  class="lazy-image"
  data-src="/images/photo.jpg"
  alt="Beautiful landscape">

<script>
const images =
  document.querySelectorAll(".lazy-image");

const imageObserver =
  new IntersectionObserver((entries, observer) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) {
        return;
      }

      const image =
        entry.target;

      image.src =
        image.dataset.src;

      image.removeAttribute("data-src");

      observer.unobserve(image);
    });
  });

images.forEach((image) => {
  imageObserver.observe(image);
});
</script>

This example loads images only when they are about to become visible. It improves page speed and reduces unnecessary network requests.

Infinite Scroll Example

<div id="items"></div>
<div id="sentinel">Loading more...</div>

<script>
const sentinel =
  document.querySelector("#sentinel");

const items =
  document.querySelector("#items");

let page = 1;

async function loadMoreItems() {
  const response =
    await fetch("/api/items?page=" + page);

  const data =
    await response.json();

  data.forEach((item) => {
    const element =
      document.createElement("p");

    element.textContent =
      item.title;

    items.appendChild(element);
  });

  page += 1;
}

const observer =
  new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      loadMoreItems();
    }
  });

observer.observe(sentinel);
</script>

A sentinel element at the bottom of the page triggers more content loading when it becomes visible.

Scroll Animation Example

const cards =
  document.querySelectorAll(".animate-card");

const animationObserver =
  new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add("is-visible");
      }
    });
  },
  {
    threshold: 0.25
  }
);

cards.forEach((card) => {
  animationObserver.observe(card);
});

This example adds a class when a card enters the viewport. CSS can then animate the element with opacity, transform, or transition effects.

Active Section Navigation Example

const sections =
  document.querySelectorAll("section[id]");

const navLinks =
  document.querySelectorAll("[data-nav-link]");

const sectionObserver =
  new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        navLinks.forEach((link) => {
          link.classList.toggle(
            "active",
            link.getAttribute("href") ===
              "#" + entry.target.id
          );
        });
      }
    });
  },
  {
    threshold: 0.6
  }
);

sections.forEach((section) => {
  sectionObserver.observe(section);
});

This pattern highlights the active navigation link as users scroll through long documentation pages or tutorials.

Play Video When Visible

const videos =
  document.querySelectorAll("video[data-autoplay]");

const videoObserver =
  new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      const video =
        entry.target;

      if (entry.isIntersecting) {
        video.play();
      } else {
        video.pause();
      }
    });
  },
  {
    threshold: 0.75
  }
);

videos.forEach((video) => {
  videoObserver.observe(video);
});

Videos can pause when off-screen and resume when visible, improving performance and reducing unnecessary CPU and battery usage.

Observe Inside Scroll Container

const scrollBox =
  document.querySelector("#scrollBox");

const target =
  document.querySelector("#item");

const observer =
  new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      console.log(entry.isIntersecting);
    });
  },
  {
    root: scrollBox,
    rootMargin: "0px",
    threshold: 1
  }
);

observer.observe(target);

The root option allows observation inside a scrollable container instead of the full browser viewport.

Intersection Observer Methods

Method Description Example Syntax
observe() Starts observing a target element. observer.observe(element)
unobserve() Stops observing a specific target. observer.unobserve(element)
disconnect() Stops observing all targets. observer.disconnect()
takeRecords() Returns pending observer entries. observer.takeRecords()

Common Intersection Observer Use Cases

Use Case How It Helps
Lazy Loading Loads images, videos, or components only when needed.
Infinite Scroll Loads more content when the user reaches the bottom.
Scroll Animations Triggers animations when elements enter the viewport.
Analytics Tracking Tracks whether content or ads were actually viewed.
Active Navigation Highlights the current section while scrolling.
Video Optimization Plays or pauses videos based on visibility.
Virtual Lists Detects visible items in long lists.
Component Hydration Loads JavaScript for components only when visible.

Intersection Observer Best Practices

  • Use Intersection Observer instead of heavy scroll event calculations.
  • Use rootMargin to load content before it becomes visible.
  • Use sensible threshold values based on the use case.
  • Call unobserve() after one-time work such as lazy loading.
  • Use disconnect() when observers are no longer needed.
  • Provide fallback loading for unsupported browsers if required.
  • Avoid triggering expensive work too frequently.
  • Respect reduced motion preferences for scroll animations.

Common Intersection Observer Mistakes

  • Forgetting to call observe() on target elements.
  • Using too many observers instead of one shared observer.
  • Not calling unobserve() for one-time lazy loading.
  • Using large thresholds without understanding visibility ratios.
  • Running expensive code every time the callback fires.
  • Ignoring browser support in legacy environments.
  • Using scroll events when Intersection Observer would be more efficient.
  • Animating content without considering accessibility and reduced motion.

Intersection Observer vs Scroll Events

Feature Intersection Observer Scroll Events
Performance Better for visibility tracking Can be expensive if not throttled
Visibility Detection Built in Manual calculations required
Lazy Loading Excellent More code required
Multiple Targets Easy to observe many elements Manual looping needed
Recommended Use Element visibility changes Continuous scroll position tracking

Key Takeaways

  • The Intersection Observer API detects when elements enter or leave view.
  • Use new IntersectionObserver() to create an observer.
  • Use observe() to start watching elements.
  • Use entry.isIntersecting and intersectionRatio to detect visibility.
  • Use unobserve() and disconnect() to clean up observers.
  • It is ideal for lazy loading, infinite scrolling, animations, tracking, and active navigation.

Pro Tip

For lazy loading, use a positive rootMargin such as "200px 0px". This starts loading content shortly before it appears on screen, creating a smoother user experience.