What is IntersectionObserver v2?
IntersectionObserver v2 is an evolution of the original Intersection Observer API that provides deeper insight into the actual visibility of elements. While the first version (v1) reports only geometric intersection—whether an element's bounding box overlaps the viewport or a specified root—v2 adds the ability to track visual occlusion caused by factors like opacity, CSS transforms, filters, and overlapping DOM elements. It introduces a new isVisible flag and optional throttling via a delay parameter, enabling developers to build more reliable, user-aware experiences.
This guide covers everything you need to know about IntersectionObserver v2: its motivation, API surface, practical code examples, and best practices for production use.
Why IntersectionObserver v2 Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Traditional intersection detection answers: "Is the element inside the viewport?" But that's only half the picture. An element might intersect the viewport yet remain completely invisible to the user because:
- It has
opacity: 0or is hidden by a CSS animation. - A modal, toast, or floating overlay sits on top of it.
- A parent has
visibility: hiddenordisplay: none. - A heavy blur filter makes it unrecognizable.
V2 solves these blind spots. When you enable visibility tracking, the observer takes into account the element's effective visual state across the entire rendering pipeline—including compositor-level effects like opacity and transforms. This is critical for analytics, ad viewability, lazy-loading of critical UI components, and any scenario where you need to know that the user can actually see the element.
How to Use IntersectionObserver v2
Enabling Visibility Tracking
To activate v2 features, you pass a new option trackVisibility (boolean) and an optional delay (milliseconds) to the observer's constructor. When trackVisibility is true, each IntersectionObserverEntry gains a boolean isVisible property, and the callback fires whenever that visibility state changes—even if the intersection ratio stays the same.
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
console.log('isVisible:', entry.isVisible);
console.log('intersectionRatio:', entry.intersectionRatio);
});
},
{
trackVisibility: true,
delay: 100 // throttle updates to at most one per 100ms
}
);
observer.observe(document.querySelector('.target'));
The delay option is a performance-conscious throttle. It limits how frequently the observer reports visibility changes, helping you avoid unnecessary work during rapid state transitions (like scrolling through a long list). A delay of 0 disables throttling, while typical values range from 50ms to 200ms.
Understanding the isVisible Property
The isVisible flag is computed based on a combination of factors:
- Opacity of the element and its ancestors (must be > 0).
- CSS transforms that affect visibility (e.g., scaling down to zero).
- Filters like
blur()oropacity()applied via CSS. - Occlusion by other elements that are painted later in the stacking order, even if they're not in the same containing block.
- The element's own
visibilityproperty.
Importantly, isVisible is independent of the geometric intersection ratio. A fully visible element (intersectionRatio = 1) can still have isVisible: false if it's hidden behind a transparent overlay with pointer-events: none? Actually, the spec accounts for visual occlusion, so an opaque overlay will mark it as not visible. The exact algorithm is defined in the spec, but for practical purposes, you can rely on it as a trustworthy signal of true user visibility.
Practical Example: Guaranteeing Lazy Image Loads Only When Truly Visible
A common use case is lazy-loading images. With v1 you might load an image as soon as it intersects the viewport. But what if a sticky banner covers the image's area? The user never sees it, yet the image loads. V2 lets you delay loading until the image becomes actually visible.
const lazyImages = document.querySelectorAll('img[data-src]');
const visibilityObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
// Load only when both intersecting AND truly visible
if (entry.isVisible && entry.intersectionRatio > 0) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
},
{
trackVisibility: true,
delay: 100,
threshold: 0.01 // any non-zero intersection
}
);
lazyImages.forEach(img => visibilityObserver.observe(img));
Notice we combine isVisible with a minimal geometric threshold. This ensures the element has entered the viewport area and is not hidden by visual effects or overlays. For a more aggressive strategy, you could even ignore intersectionRatio entirely and rely solely on isVisible, but that might trigger loads when the element is out of view yet somehow visible (rare, but possible via transforms). Combining both is the safest.
Tracking Visibility-Only Changes
Sometimes you only care about visibility changes, not geometric intersection. For example, you want to pause an animation when the element becomes obscured by an ad overlay, regardless of scrolling. V2 lets you set a high root margin to effectively ignore viewport intersection and only react to visibility.
const pauseController = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const player = entry.target.querySelector('video');
if (player) {
entry.isVisible ? player.play() : player.pause();
}
});
},
{
trackVisibility: true,
delay: 0,
// Make root margins huge so geometric intersection is always true
rootMargin: '1000px 1000px 1000px 1000px'
}
);
document.querySelectorAll('.video-container').forEach(el => pauseController.observe(el));
By inflating the root margins, we ensure the observer always considers the element "intersecting", thus the callback fires only when isVisible toggles—effectively a pure visibility observer.
Best Practices
-
Progressive enhancement: IntersectionObserver v2 is currently supported in Chromium-based browsers (Chrome, Edge, Opera) and behind the "Experimental Web Platform features" flag in some versions. Always feature-detect
trackVisibilitysupport and fall back to standard IntersectionObserver if unavailable. -
Mind the delay: A delay value that's too high can cause missed visibility states. If you need immediate reaction (e.g., pausing a video the moment it's covered), use
delay: 0. For analytics or lazy-loading, a small delay (50-100ms) reduces CPU spikes. -
Combine with geometric thresholds: For most practical applications, use both
isVisibleand intersection ratio thresholds. This guards against edge cases where an element is considered "visible" due to compositor quirks but actually outside the viewport. -
Performance considerations: The visibility computation relies on the compositor and may involve additional work. Avoid observing hundreds of elements simultaneously with
trackVisibility: true. Prefer scoping to a few critical elements (videos, ads, hero images). -
Test with real overlays: Simulate modals, cookie banners, and sticky headers in your tests. Ensure
isVisiblebehaves as expected when elements are obscured by different types of DOM siblings and fixed-position elements.
Feature Detection and Fallback
To safely use v2 in production, wrap your observer creation in a capability check. The snippet below attempts v2 and falls back to v1 gracefully.
function createVisibilityObserver(callback, options = {}) {
// Default v2 options
const v2Options = {
trackVisibility: true,
delay: options.delay ?? 100,
...options
};
// Try to create a v2 observer by checking if 'trackVisibility' is accepted
try {
const testObserver = new IntersectionObserver(() => {}, v2Options);
// If no error and the option is reflected (Chrome), use v2
if (testObserver.trackVisibility !== undefined) {
testObserver.disconnect(); // clean up test
return new IntersectionObserver(callback, v2Options);
}
} catch (e) {
// Fallback to v1
}
// Fallback: use standard IntersectionObserver with v1 options only
const v1Options = { ...options };
delete v1Options.trackVisibility;
delete v1Options.delay;
return new IntersectionObserver(callback, v1Options);
}
// Usage
const observer = createVisibilityObserver((entries) => {
entries.forEach(entry => {
// entry.isVisible might be undefined in fallback, handle gracefully
const visible = entry.isVisible !== undefined ? entry.isVisible : entry.isIntersecting;
if (visible) {
// do something
}
});
}, { threshold: 0.1, delay: 50 });
observer.observe(document.getElementById('ad-unit'));
Conclusion
IntersectionObserver v2 fills a critical gap left by its predecessor—answering not just whether an element sits inside the viewport, but whether the user can actually see it. By leveraging trackVisibility and delay, you can build smarter lazy-loading, precise viewability analytics, and adaptive UI behaviors that respond to real-world visual conditions. While browser support is still evolving, feature detection and progressive enhancement make it safe to adopt today. Start with the most impactful elements, combine visibility and intersection checks, and enjoy a new level of control over what your users truly experience.