The Intersection Observer API is a modern browser API that lets you detect when an element enters or leaves the viewport, without resorting to scroll event listeners that fire hundreds of times per second and trash performance. The classic use case is lazy-loading images — as WordPress 5.5 added native lazy loading via the loading="lazy" attribute, the Intersection Observer has become most valuable for scroll-triggered animations, infinite scroll pagination, ad impression tracking, and loading heavy third-party embeds (maps, videos, social widgets) only when they scroll into view. The API is remarkably simple: you create an IntersectionObserver instance with a callback function, then call .observe(element) on each element you want to watch. The callback fires with an array of IntersectionObserverEntry objects, each with a boolean isIntersecting property and an intersectionRatio between 0 and 1. The threshold option controls at what visibility percentage the callback fires; the rootMargin option lets you trigger the callback slightly before the element enters the viewport (e.g. ‘200px 0px’ to start loading 200 pixels before the element is visible). Combining this with CSS transition classes gives you smooth, performant scroll-triggered animations that were previously only achievable with heavy JavaScript libraries. For a complementary technique on the input side, see the event delegation guide.
Problem: You want to trigger animations or load heavy content (videos, maps) only when elements scroll into the viewport, without degrading scroll performance with event listeners.
Solution: Add the following code to your theme’s JavaScript file:
// Scroll-triggered CSS animations
// Add class="animate-on-scroll" to any element in your HTML
// CSS: .animate-on-scroll { opacity:0; transform:translateY(30px); transition:all .6s ease; }
// .animate-on-scroll.visible { opacity:1; transform:none; }
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target); // stop watching once animated
}
});
},
{ threshold: 0.15 } // fire when 15% of the element is visible
);
document.querySelectorAll('.animate-on-scroll').forEach(el => observer.observe(el));
// Lazy-load a heavy iframe (YouTube embed)
// HTML: <div class="lazy-iframe" data-src="https://www.youtube.com/embed/VIDEO_ID"></div>
const iframeObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const wrapper = entry.target;
const iframe = document.createElement('iframe');
iframe.src = wrapper.dataset.src;
iframe.width = '560';
iframe.height = '315';
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.allowFullscreen = true;
wrapper.replaceWith(iframe);
iframeObserver.unobserve(wrapper);
});
},
{ rootMargin: '200px 0px' } // start loading 200px before entering the viewport
);
document.querySelectorAll('.lazy-iframe').forEach(el => iframeObserver.observe(el));
// Infinite scroll: load more posts when the sentinel enters the viewport
const sentinel = document.querySelector('#load-more-sentinel');
let page = 2;
const infiniteObserver = new IntersectionObserver(async (entries) => {
if (!entries[0].isIntersecting) return;
const res = await fetch(`/wp-json/wp/v2/posts?page=${page}&per_page=5`);
const posts = await res.json();
if (!posts.length) { infiniteObserver.disconnect(); return; }
posts.forEach(p => {
const li = document.createElement('li');
li.innerHTML = `<a href="${p.link}">${p.title.rendered}</a>`;
document.querySelector('#post-list').appendChild(li);
});
page++;
});
if (sentinel) infiniteObserver.observe(sentinel);
NOTE: Always call observer.unobserve(entry.target) after an animation fires if you only want it to happen once — otherwise the callback fires again if the element scrolls back out of and into the viewport. For the infinite scroll pattern, disconnect the observer completely (observer.disconnect()) when the API returns an empty array, indicating there are no more posts to load. The Intersection Observer API is supported in all modern browsers with no polyfill required for current WordPress projects.