Zero Sized Intersection Observers
Apr 06, 2024A common strategy to implement infinite-scroll is to place a marker
element at the end of a scroll container. When the user scrolls to the bottom
of the container, the marker element becomes visible and triggers
an IntersectionObserver
callback, at which point you can fetch the next page of data.
<ul>
<li>One</li>
<li>Two</li>
...
<li>Fifty</li>
<div id="loadMore"></div>
</ul>
const target = document.getElementById('loadMore');
const observer = new IntersectionObserver((entries) => {
if (entries.some(e => e.isIntersecting)) {
// fetch next page
}
});
observer.observe(target);
Since you usually don’t want the marker element to be visible to the user, it is common to leave it empty. Depending on your browser, this will seem to work just fine.
However, if we read the
IntersectionObserver
spec
carefully, we see that the default threshold
value of 0
signifies
an intersection of “any non-zero number of pixels”. This means that
(in browsers that satisfy the spec) our callback will never fire
because the target is zero pixels wide and zero pixels tall.
The w3c seems to have added the
isIntersecting
Flag
partially for this purpose.
But never clearly disambiguated how zero-sized elements should interact with
the threshold
in order to trigger the observer callback. Because
of this, we see platform-dependent behavior with this edge case.
According to my testing, the code above will trigger the observer callback on MacOS (Firefox and Chrome) but not on Windows (Chrome). In order to ensure consistent behavior across platforms, developers should always add some content to elements observed by the IntersectionObserver API.
I would recommend an implementation that can fall back to a plain button
if the user’s browser does not support the IntersectionObserver
API.
<ul>
<li>One</li>
<li>Two</li>
...
<li>Fifty</li>
<button id="loadMore">Load More</button>
</ul>
const loadNextPage = () => {
/// ...
};
const target = document.getElementById('loadMore');
target.onclick = () => {
loadNextPage();
};
if ('IntersectionObserver' in window) {
target.style = 'visibility: hidden';
const observer = new IntersectionObserver((entries) => {
if (entries.some(e => e.isIntersecting)) {
loadNextPage();
}
});
observer.observe(target);
}