Introduction to IntersectionObserver v2
IntersectionObserver v2 builds on the original IntersectionObserver API to deliver deeper visibility insights. While v1 tells you when an element enters or exits the viewport, v2 adds the ability to know if the element is actually visible to the user — taking into account occlusions, CSS transforms, opacity, and more. This tutorial covers everything you need to implement v2 in modern web applications.
What Is IntersectionObserver v2?
IntersectionObserver v2 is an extension of the original IntersectionObserver specification. The core addition is the isVisible property on each IntersectionObserverEntry. When you configure an observer with the trackVisibility option, the browser will compute a boolean indicating whether the observed element is currently “visible” from the user’s perspective. This computation accounts for:
- Occlusion by other elements — if another element overlaps and hides the target
- CSS properties like
opacity,transform,visibility, andz-index - Browser UI — elements covered by system dialogs or scroll bars
- Cross-origin restrictions — elements in iframes may have limited visibility info
The v2 API introduces two new configuration options:
trackVisibility: true— enables the computation ofisVisibledelay— a minimum interval (in milliseconds) between successive visibility computations to avoid performance hits
Why It Matters: Enhanced Visibility Insights
Standard IntersectionObserver (v1) only tells you whether an element intersects the viewport. An element could be inside the viewport but completely invisible because:
- It’s hidden behind a modal dialog
- Its opacity is set to 0
- A sticky banner covers it entirely
- CSS transforms have moved it off-screen visually while still technically intersecting
For many use cases — ad impression tracking, analytics, lazy-loading UI components, and more — you need to know whether the user can actually see the element. v2 fills that gap. It enables:
- More accurate ad viewability measurement
- Precise analytics on user attention (e.g., which parts of a dashboard are truly visible)
- Conditional rendering or data fetching only when content is visible and not just “in viewport”
- Performance optimizations: avoid CPU/GPU work for elements that are occluded
Core Concepts and API
The v2 API remains largely consistent with v1. The key differences are the constructor options and the isVisible property. Here’s a quick comparison:
- v1:
new IntersectionObserver(callback, { threshold, root, rootMargin })— callback receives entries withisIntersecting,intersectionRatio, etc. - v2: same constructor, but with added options
trackVisibilityanddelay. Each entry now also exposesisVisible(a boolean).
Important: isVisible is only populated when trackVisibility is true. If you do not set that option, isVisible will be undefined.
Implementing IntersectionObserver v2: Step-by-Step
1. Feature Detection
Not all browsers support v2 yet. Before using it, check whether the browser’s IntersectionObserver implementation includes the trackVisibility capability. A robust detection method is to attempt constructing an observer with the option and catch errors:
function supportsIntersectionObserverV2() {
if (!('IntersectionObserver' in window)) return false;
try {
// Create a dummy observer with trackVisibility
new IntersectionObserver(() => {}, { trackVisibility: true, delay: 100 });
return true;
} catch (e) {
return false;
}
}
if (supportsIntersectionObserverV2()) {
console.log('v2 is supported!');
} else {
console.log('Fall back to v1 or other visibility checks.');
}
You can also check for the prototype property if you prefer a non-try/catch approach, but the above covers edge cases like missing constructor options gracefully.
2. Configuring the Observer
Create an observer with both v1 and v2 options. The delay option helps throttle the visibility computations. A typical setup:
const observerOptions = {
root: null, // viewport
rootMargin: '0px',
threshold: 0, // fire as soon as 1px intersects (or use [0, 1])
trackVisibility: true, // enable v2 visibility tracking
delay: 100 // minimum 100ms between visibility evaluations
};
const observer = new IntersectionObserver(onIntersection, observerOptions);
3. Handling Callbacks
The callback receives an array of IntersectionObserverEntry objects. Each entry now contains:
isIntersecting— same as v1, true when the element intersects the rootintersectionRatio— fraction of the element visible within the rootisVisible— (v2 only) true if the element is actually visible to the user, considering occlusions, opacity, etc.target— the observed element- Other standard properties (
boundingClientRect,intersectionRect, etc.)
A common pattern is to combine both properties for a more nuanced understanding:
function onIntersection(entries) {
entries.forEach(entry => {
const { target, isIntersecting, isVisible } = entry;
// Element is in the viewport but might be occluded
if (isIntersecting) {
if (isVisible === true) {
console.log(`${target.id} is visible to the user.`);
// Trigger expensive operations here
} else if (isVisible === false) {
console.log(`${target.id} is in viewport but hidden (occluded, opacity 0, etc.).`);
// Defer work or clean up
} else {
// isVisible undefined – v2 not supported or not yet computed
console.log(`${target.id} is intersecting, visibility unknown.`);
}
} else {
console.log(`${target.id} is outside the viewport.`);
// Pause work, free resources
}
});
}
4. Practical Example: Lazy-Loading Images with Visibility Tracking
Let’s build a lazy-loading image component that loads images only when they are both intersecting the viewport and truly visible to the user. This avoids loading images that are hidden behind a modal or an overlay.
<!-- HTML -->
<img data-src="photo.jpg" class="lazy-img" alt="A lazy-loaded photo">
<div class="overlay-banner">Temporary promotional banner</div>
<style>
.lazy-img { width: 300px; height: 200px; background: #eee; }
.overlay-banner {
position: fixed; top: 0; left: 0; width: 100%; height: 60px;
background: rgba(0,0,0,0.8); z-index: 999; color: white; display: flex; align-items: center;
}
</style>
<script>
// Feature detection and fallback
const supportsV2 = (() => {
if (!('IntersectionObserver' in window)) return false;
try {
new IntersectionObserver(() => {}, { trackVisibility: true, delay: 100 });
return true;
} catch (e) { return false; }
})();
const observerOptions = {
rootMargin: '200px', // pre-load margin
threshold: 0,
...(supportsV2 && { trackVisibility: true, delay: 100 })
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const img = entry.target;
const shouldLoad = entry.isIntersecting &&
(supportsV2 ? entry.isVisible !== false : true); // fallback: assume visible if no v2
if (shouldLoad && img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img); // stop observing once loaded
}
});
}, observerOptions);
document.querySelectorAll('.lazy-img').forEach(img => observer.observe(img));
</script>
In this example, images under a fixed overlay banner will not load until the banner is dismissed (or the user scrolls past it, revealing the images). The fallback gracefully handles browsers without v2 by simply loading on intersection.
Best Practices
Use v2 When You Need Deeper Visibility Information
Don’t enable trackVisibility indiscriminately. It triggers additional browser calculations (layout, style, and layer tree inspections). Reserve it for cases where knowing the true visibility adds value — ad tracking, analytics, critical lazy-loading, or performance optimizations that depend on actual screen presence.
Combine with v1 for Full Coverage
Always check isIntersecting alongside isVisible. An element can be outside the viewport (non-intersecting) and isVisible may be true in certain edge cases (e.g., when the element is partially off-screen but the visible part is considered “visible” by the algorithm). Use both to form a complete picture.
Avoid Over-Observing
Observing too many elements with trackVisibility: true can cause performance bottlenecks. Use the delay option to throttle updates. Consider grouping observations or only observing elements that are likely to be visible (e.g., after they start intersecting). Unobserve elements as soon as you no longer need the data.
Handle Callback Performance
The callback runs on every visibility change. Keep it lightweight — avoid heavy DOM manipulations, synchronous style reads, or complex state updates. If you need to react, consider batching updates using requestAnimationFrame or scheduling tasks with setTimeout(0).
Respect User Privacy
Browsers enforce privacy restrictions on v2. The isVisible computation may be disabled when:
- The page is in a background tab
- The user hasn’t interacted with the page recently
- The observed element is inside a cross-origin iframe
You might receive entries where isVisible is undefined even if trackVisibility is enabled. Always code defensively and treat undefined as “unknown visibility.” Never rely on isVisible for security or payment logic — it’s an optimization hint, not a guarantee.
Conclusion
IntersectionObserver v2 adds a crucial layer of visibility intelligence to modern web applications. By telling you not just whether an element intersects the viewport, but whether it’s truly visible to the user, it unlocks more accurate ad tracking, smarter lazy-loading, and deeper performance optimizations. Implement feature detection, combine isIntersecting with isVisible, respect the performance trade-offs, and always handle privacy-driven “unknown” states. With these techniques, you can deliver faster, more user-centric experiences while gathering better analytics.