Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IntersectionObserver does not intercept elements with display contents

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:

  • Is this indeed the intended behaviour or an unwanted side-effect?
  • If it is the intended behaviour - what are the proper use-cases for display-contents?
like image 655
Daniel Avatar asked Dec 05 '25 04:12

Daniel


1 Answers

[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:

  • Making a <ul> act as its <li> children in CSS Grid.
  • Flattening accessibility trees (e.g., for screen readers).
  • Not for hiding containers while tracking visibility—that’s what opacity: 0 or visibility: hidden + bounding box are for.
like image 77
longteng yi Avatar answered Dec 07 '25 17:12

longteng yi