It looks like IntersectionObserver never considers elements with display: contents in the viewport.
This behaviour makes sense for display: none but I find it counter-intuitive for display: contents.
The use case of having a clean wrapper which only fetches & renders its contents on viewport intersection seems valid to me.
Example
function callback(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
document.getElementById('content').style.display = 'block';
document.getElementById('loader').style.display = 'none';
}
});
};
const observer = new IntersectionObserver(callback);
const target = document.getElementById('wrapper');
observer.observe(target);
#wrapper {
display: contents;
}
#content {
display: none;
}
<div id="wrapper">
<div id="loader">Loading...</div>
<div id="content">Shown on viewport intersection</div>
</div>
Looking at the Mozilla docs I can see that:
These elements don't produce a specific box by themselves
But still I'd expect IntersectionObserver to be able to detect it.
So guess it's a 2 part question:
display-contents?[part1] This issue is by design:
display: contents makes the element vanish from the layout tree (it has no box model), so IntersectionObserver can't track it. The spec explicitly states observers require a bounding box, which display: contents elements don't have. This isn't like display: none (which is tracked but reports isIntersecting: false); here, the element literally doesn't exist as a renderable box.
Why
Your #wrapper with display: contents is ignored because it contributes nothing to the layout—it's as if you wrote:
<div id="loader">Loading...</div>
<div id="content">Shown on viewport intersection</div>
The observer has no physical area to measure.
Fix
Target the #loader (which is visible) since it occupies space while loading. Once it intersects, swap content and disconnect:
function callback(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
document.getElementById('content').style.display = 'block';
document.getElementById('loader').style.display = 'none';
observer.unobserve(entry.target); // Clean up
}
});
}
const observer = new IntersectionObserver(callback);
observer.observe(document.getElementById('loader')); // Observe LOADER, not wrapper
#wrapper {
display: contents; /* Optional now—works with or without */
}
#content {
display: none;
}
[part2] Proper use cases for display: contents:
It's meant for structural layout tweaks, not visibility control, such as:
<ul> act as its <li> children in CSS Grid.opacity: 0 or visibility: hidden + bounding box are for.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With