Observing Rendered DOM Nodes

Sam Thorogood has been looking into all kinds of changes that can happen to DOM Nodes:

This post will explain how to be notified when:

  • an element is added or removed from the DOM
  • the bounding box of an element changes (i.e., resizes)
  • an element moves around the page for any reason

Expect some ResizeObserver, IntersectionObserver, and MutationObserver sprinkled all over the post.

Observing rendered DOM nodes →

Related: On Twitter Denis Radin chimed in with his StyleObserver.js to track style changes.

content-visiblity: auto; vs. jumpy scrollbars, a solution

As warned in content-visibility: the new CSS property that boosts your rendering performance you need to be careful with applying content-visibility: auto; on each and every element as the scrollbar might get jumpy.

This is because elements will be rendered as they scroll into the viewport and will be hidden as they scroll out of the viewport, thereby affecting the height of the rendered page, and thus also affecting the scrollbar.

ℹ️ Apart from a jumpy scrollbar it can also negatively affect accessibility when you include headings and landmark elements inside of regions styled with content-visibility: auto;. See Content-visibility and Accessible Semantics for details.

Now, thanks to infinite scroll we are — or at least I am — kind of used to the thumb part of the scrollbar shrinking and jumping back up a bit on the scrollbar track as you scroll down. What we’re not used to is the thumb part jump forwards on the scroll track as you scroll down. This is because elements that slide out of the viewport will no long be rendered — as that’s what content-visibility: auto; does — and the scrollbar is (by default) only calculated against the rendered elements.


Elements can become non-rendered elements as they scroll out of the viewport,
thanks to content-visibility: auto; doing its thing.

To cater for this jumpy behavior you should use contain-intrinsic-size so space for an element is reserved when it’s not being rendered. However, it is not always possible to know a box its dimensions in advance. Looking for a way to automatically reserve space for previously rendered elements, Alex Russel created a little script for it.

One challenge with naive application of content-visibility, though, is the way that it removes elements from the rendered tree once they leave the viewport — particularly as you scroll downward. If the scroll position depends on elements above the currently viewable content “accordion scrollbars” can dance gleefully as content-visibility: auto does its thing.

In a first version of the script he applied content-visibility: visible on each element from the moment it had appeared on screen. To detect this an IntersectionObserver is used. While this does prevent the scrollbar thumb from jumping forwards as you scroll down, it will make the page slow again as that content remains rendered (even though it’s off-screen).

A second version of the script takes a different approach and calculates the contain-intrinsic-size to apply based on the element’s dimensions. That way elements that passed by once now have a correct contain-intrinsic-size set, and can safely be hidden again as content-visibility: auto does its job.

let spaced = new WeakMap();
let reserveSpace = (el, rect) => {
    let old = spaced.get(el);
    // Set intrinsic size to prevent jumping.
    if (!old || rectNotEQ(old, rect)) {
        spaced.set(el, rect);
        el.attributeStyleMap.set(
        "contain-intrinsic-size",
        `${rect.width}px ${rect.height}px`
        );
    }
};

Additionally he also added a ResizeObserver to cater for resize events.

Resize-Resilient `content-visiblity` Fixes →

🤔 Clever script indeed, yet I cannot help but think: this should be possible without the needs for this extra script. What if a value like contain-intrinsic-size: auto; would be allowed, and do exactly as the script Alex built does?

UPDATE 2020.12.21: I’ve created an issue on GitHub that proposes contain-intrinsic-size: auto;. Let’s see where this goes …

UPDATE 2021.01.13: Looks like support for contain-intrinsic-size: auto; will land in the spec!

Container Queries with the <watched-box> Custom Element

Heydon Pickering has created <watched-box>:

I wanted a simple, declarative container queries solution, and here it is:

  • ❤ Custom Element + ResizeObserver
  • 🥣 Use and mix together any CSS length units
  • 🖼 Orientation supported
  • 🧚‍♀️ ≅1.5KB minified

Once imported you can use it as follows:

<watched-box widthBreaks="70ch, 900px" heightBreaks="50vh, 60em">
  <!-- HTML and text stuff here -->
</watched-box>

<watched-box> will then automatically add the proper classes:

  • Less than or equal to the supplied width: w-lte-[the width]
  • Greater than the supplied width: w-gt-[the width]
  • Less than or equal to the supplied height: h-lte-[the height]
  • Greater than the supplied height: h-gt-[the height]

<watched-box>