SalakCode SalakCode
Browser APIs

Intersection Observer

Detect when elements enter or leave the viewport

Intermediate
browser api performance lazy-loading

Definition

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport. It’s the modern, performant replacement for scroll event listeners.

Basic Usage

Creating an Observer

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('Element is visible:', entry.target);
      // Trigger action
    }
  });
}, {
  root: null,        // viewport
  rootMargin: '0px',
  threshold: 0.5     // 50% visibility
});

// Start observing
const target = document.querySelector('#target');
observer.observe(target);

IntersectionEntry Properties

{
  target: Element,           // The observed element
  isIntersecting: Boolean,   // Currently intersecting?
  intersectionRatio: Number, // 0 to 1 (visibility percentage)
  intersectionRect: DOMRect, // Visible area rectangle
  boundingClientRect: DOMRect, // Target's bounds
  rootBounds: DOMRect,       // Root's bounds
  time: DOMHighResTimeStamp  // When intersection changed
}

Common Use Cases

Lazy Loading Images

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: '50px' }); // Start loading 50px before visible

document.querySelectorAll('img.lazy').forEach(img => {
  imageObserver.observe(img);
});

Infinite Scroll

const sentinel = document.querySelector('#scroll-sentinel');

const scrollObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting && !isLoading) {
      loadMoreContent();
    }
  });
}, { rootMargin: '200px' });

scrollObserver.observe(sentinel);

Animate on Scroll

const animationObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('animate-in');
      // Optional: unobserve after animation
      // animationObserver.unobserve(entry.target);
    }
  });
}, { threshold: 0.2 });

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  animationObserver.observe(el);
});

Configuration Options

Threshold Values

// Single threshold - trigger at 50% visibility
{ threshold: 0.5 }

// Multiple thresholds - trigger at multiple points
{ threshold: [0, 0.25, 0.5, 0.75, 1] }

// Zero threshold - trigger as soon as 1px is visible
{ threshold: 0 }

Root Margin

// Expand the root by 100px on all sides
{ rootMargin: '100px' }

// Top right bottom left (like CSS margin)
{ rootMargin: '100px 50px 75px 25px' }

// Negative margin - shrink the root
{ rootMargin: '-100px' }

// Combined with threshold
{ 
  rootMargin: '50px',
  threshold: 0.5 
}

Custom Root

// Observe within a scrollable container
const container = document.querySelector('.scroll-container');

const observer = new IntersectionObserver(callback, {
  root: container,
  threshold: 0.5
});

Advanced Patterns

Sticky Header Detection

const header = document.querySelector('header');
const sentinel = document.createElement('div');
sentinel.style.position = 'absolute';
sentinel.style.top = '0';
sentinel.style.height = '1px';
document.body.prepend(sentinel);

const observer = new IntersectionObserver((entries) => {
  const isSticky = !entries[0].isIntersecting;
  header.classList.toggle('is-sticky', isSticky);
});

observer.observe(sentinel);

Section Spy (Active Navigation)

const sections = document.querySelectorAll('section');
const navLinks = document.querySelectorAll('.nav-link');

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

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

Video Autoplay/Pause

const videoObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const video = entry.target;
    
    if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
      video.play();
    } else {
      video.pause();
    }
  });
}, { threshold: 0.5 });

document.querySelectorAll('video').forEach(video => {
  videoObserver.observe(video);
});

Performance Considerations

Batch Processing

// Observe multiple elements efficiently
const observer = new IntersectionObserver((entries) => {
  // All entries fire in the same callback
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadElement(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

// Observe all at once
document.querySelectorAll('.lazy').forEach(el => {
  observer.observe(el);
});

Cleanup

// Stop observing specific element
observer.unobserve(target);

// Stop observing all elements
observer.disconnect();

// In React/Vue components
useEffect(() => {
  const observer = new IntersectionObserver(callback);
  observer.observe(ref.current);
  
  return () => observer.disconnect(); // Cleanup
}, []);

Browser Support

// Feature detection
if ('IntersectionObserver' in window) {
  // Use Intersection Observer
} else {
  // Fallback to scroll events or polyfill
  import('intersection-observer');
}
Key Takeaway

Intersection Observer replaces scroll event listeners for visibility detection, providing better performance and simpler code. Use it for lazy loading, infinite scroll, scroll animations, and tracking element visibility. Always clean up observers when components unmount to prevent memory leaks.

Resources

Related Topics