Intersection Observer v2 adds the capability to not only observe intersections per se, but to also detect if the intersecting element was visible at the time of intersection.
Intersection Observer v1 is one of those APIs that's probably universally loved, and, now that
Safari supports it as well,
it's also finally universally usable in all major browsers. For a quick refresher of the API,
I recommend watching Surma's
Supercharged Microtip on Intersection
Observer v1 that is embedded below.
You can also read Surma's in-depth
article.
People have used Intersection Observer v1 for a wide range of use cases like
lazy loading of images and videos,
being notified when elements reach position: sticky
,
firing analytics events,
and many more.
For the full details, check out the Intersection Observer docs on MDN, but as a short reminder, this is what the Intersection Observer v1 API looks like in the most basic case:
const onIntersection = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
console.log(entry);
}
}
};
const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));
What's challenging with Intersection Observer v1?
To be clear, Intersection Observer v1 is great, but it's not perfect. There are
some corner cases where the API falls short. Let's have a closer look!
The Intersection Observer v1 API can tell you when an element is scrolled into the
window's viewport, but it doesn't tell you whether the element is covered
by any other page content (that is, when the element is occluded) or whether
the element's visual display has been modified by visual effects like transform
, opacity
,
filter
, etc., which effectively can make it invisible.
For an element in the top-level document, this information can be determined by analyzing
the DOM via JavaScript, for example via
DocumentOrShadowRoot.elementFromPoint()
and then digging deeper.
In contrast, the same information cannot be obtained if the element in question is
located in a third-party iframe.
Why is actual visibility such a big deal?
The Internet is, unfortunately, a place that attracts bad actors with worse intentions.
For example, a shady publisher that serves pay-per-click ads on a content site might be incentivized
to trick people into clicking their ads to increase the publisher's ad payout (at least
for a short period, until the ad network catches them).
Typically, such ads are served in iframes.
Now if the publisher wanted to get users to click such ads, they could make the ad iframes
completely transparent by applying a CSS rule iframe { opacity: 0; }
and overlaying the iframes
on top of something attractive, like a cute cat video that users would actually want to click.
This is called clickjacking.
You can see such a clickjacking attack in action in the upper section of this
demo (try "watching" the cat video
and activate "trick mode").
You will notice that the ad in the iframe "thinks" it received legitimate clicks, even if it was
completely transparent when you (pretend-involuntarily) clicked it.
How does Intersection Observer v2 fix this?
Intersection Observer v2 introduces the concept of tracking the actual "visibility" of a target
element as a human being would define it.
By setting an option in the
IntersectionObserver
constructor,
intersecting
IntersectionObserverEntry
instances will then contain a new boolean field named isVisible
.
A true
value for isVisible
is a strong guarantee from the underlying implementation
that the target element is completely unoccluded by other content
and has no visual effects applied that would alter or distort its display on screen.
In contrast, a false
value means that the implementation cannot make that guarantee.
An important detail of the
spec
is that the implementation is permitted to report false negatives (that is, setting isVisible
to false
even when the target element is completely visible and unmodified).
For performance or other reasons, browsers limit themselves to working with bounding
boxes and rectilinear geometry; they don't try to achieve pixel-perfect results for
modifications like border-radius
.
That said, false positives are not permitted under any circumstances (that is, setting
isVisible
to true
when the target element is not completely visible and unmodified).
What does the new code look like in practice?
The IntersectionObserver
constructor now takes two additional configuration properties: delay
and trackVisibility
.
The delay
is a number indicating the minimum delay in milliseconds between notifications from
the observer for a given target.
The trackVisibility
is a boolean indicating whether the observer will track changes in a target's
visibility.
It's important to note here that when trackVisibility
is true
, delay
is required to be at
least 100
(that is, no more than one notification every 100ms).
As noted before, visibility is expensive to calculate, and this requirement is a precaution against
performance degradation and battery consumption. The responsible developer will use the
largest tolerable value for delay.
According to the current spec, visibility is calculated as follows:
If the observer's
trackVisibility
attribute isfalse
, then the target is considered visible. This corresponds to the current v1 behavior.If the target has an effective transformation matrix other than a 2D translation or proportional 2D upscaling, then the target is considered invisible.
If the target, or any element in its containing block chain, has an effective opacity other than 1.0, then the target is considered invisible.
If the target, or any element in its containing block chain, has any filters applied, then the target is considered invisible.
If the implementation cannot guarantee that the target is completely unoccluded by other page content, then the target is considered invisible.
This means current implementations are pretty conservative with guaranteeing visibility.
For example, applying an almost unnoticeable grayscale filter like filter: grayscale(0.01%)
or setting an almost invisible transparency with opacity: 0.99
would all render the element
invisible.
Below is a short code sample that illustrates the new API features. You can see its click tracking logic in action in the second section of the demo (but now, try "watching" the puppy video). Be sure to activate "trick mode" again to immediately convert yourself into a shady publisher and see how Intersection Observer v2 prevents non-legitimate ad clicks from being tracked. This time, Intersection Observer v2 has our back! 🎉
<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.
// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;
// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;
const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
if ((visibleSince > 0) &&
(performance.now() - visibleSince >= minimumVisibleDuration)) {
trackAdClick();
} else {
rejectAdClick();
}
});
const observer = new IntersectionObserver((changes) => {
for (const change of changes) {
// ⚠️ Feature detection
if (typeof change.isVisible === 'undefined') {
// The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
change.isVisible = true;
}
if (change.isIntersecting && change.isVisible) {
visibleSince = change.time;
} else {
visibleSince = 0;
}
}
}, {
threshold: [1.0],
// 🆕 Track the actual visibility of the element
trackVisibility: true,
// 🆕 Set a minimum delay between notifications
delay: 100
}));
// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));
Related Links
- Latest Editor's Draft of the Intersection Observer spec.
- Intersection Observer v2 on Chrome Platform Status.
- Intersection Observer v2 Chromium bug.
- Blink Intent to Implement posting.
Acknowledgements
Thanks to Simeon Vincent, Yoav Weiss, and Mathias Bynens for reviewing this article, as well as Stefan Zager likewise for reviewing and for implementing the feature in Chrome. Hero image by Sergey Semin on Unsplash.