Contain-Intrinsic-Size and Scroll Anchoring

contain-intrinsic-size gives a content-visibility-skipped subtree a placeholder size so it still reserves scroll height while its real content is not rendered; the auto keyword makes the browser remember each subtree’s last-rendered size and reuse it. Getting this estimate right is what prevents scrollbar jumps and scroll-anchoring shifts that surface as Cumulative Layout Shift. This guide covers the sizing mechanics and the CLS failure mode. It builds on Content-Visibility and Rendering Subtrees, part of Layout and Paint Optimization.

Why a Placeholder Size Is Needed

When content-visibility: auto skips a subtree, its descendants are not laid out, so without an intrinsic size the box collapses to near-zero height. The document’s total scroll height then represents only the rendered sections. As the user scrolls and skipped sections render, the page grows, the scrollbar thumb resizes, and the viewport content jumps β€” the classic scrollbar lurch.

/* ❌ No reserved size: skipped sections collapse, scroll height unstable */
.chapter { content-visibility: auto; }

/* βœ… Reserve a height estimate so scroll height stays stable */
.chapter {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px; /* placeholder until really rendered */
}

The two-value auto 800px form means: use the remembered last-rendered size if one exists, otherwise fall back to 800px. The single-axis or two-axis forms (contain-intrinsic-size: 320px 800px) set width and height explicitly when content is uniform.

The auto Keyword

Plain contain-intrinsic-size: 800px uses 800px every time the subtree is skipped, even after the browser has seen its real height. If the estimate is wrong, the page shifts each time a section is skipped and re-rendered. The auto keyword fixes this: after a subtree renders once, the browser stores its actual size and substitutes that for the placeholder on subsequent skips.

/* The browser records the real height after first render and reuses it */
.chapter {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px; /* 800px only until first real render */
}

After the first pass, scroll height reflects true content heights, so scrolling back and forth no longer drifts.

Scroll Anchoring and CLS

Browsers run scroll anchoring to keep the visual viewport stable when content above the scroll position changes size. A content-visibility subtree whose placeholder estimate differs from its real height changes size exactly when it renders β€” and if that happens above the current scroll position, scroll anchoring has to compensate. When the compensation is imperfect or the shift is below the anchor, the result is a visible jump that the Layout Instability API records as CLS.

[Layout Shift]  chapter #7 rendered
β”œβ”€ placeholder height: 800px (estimate)
β”œβ”€ real height:        1180px
β”œβ”€ delta:              +380px above viewport
└─ score: 0.18  ← exceeds 0.1 CLS target, hadRecentInput: false

The cure is to make the estimate close to reality (or let auto learn it) so the delta is small or zero.

/* Before: fixed estimate far from real height β†’ CLS on render */
.chapter { content-visibility: auto; contain-intrinsic-size: 800px; }

/* After: auto remembers true size; estimate only matters for first paint */
.chapter { content-visibility: auto; contain-intrinsic-size: auto 1100px; }

Pick the fallback from a measured median of your real content heights so even the first render shifts as little as possible.

Measuring the Shift

Attribute shifts to specific skipped subtrees with the Layout Instability API, reading sources to see which node moved.

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue // ignore user-driven shifts
    for (const s of entry.sources) {
      console.log('shifted', s.node, 'by', entry.value)
    }
  }
}).observe({ type: 'layout-shift', buffered: true })

The full attribution workflow for these entries β€” distinguishing input-driven from layout-driven shifts and turning sources into actionable nodes β€” is in Debugging CLS with the Layout Instability API.

Verification Checklist

Metric Target How measured
CLS from skipped-subtree render ≀ 0.1 Layout Instability API
Scroll height stability No jump on scroll Visual / scrollbar thumb
Placeholder-to-real delta Near 0 after first render contain-intrinsic-size: auto
  • contain-intrinsic-size uses the auto
  • No layout-shift entries with hadRecentInput: false