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 — the same mechanism covered in Using content-visibility for Offscreen Content — 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.

Collapsed versus reserved scroll height Without an intrinsic size skipped subtrees collapse and the scroll thumb lurches; a reserved placeholder keeps total scroll height stable. No intrinsic size contain-intrinsic-size: auto 800px rendered section skipped → ~0px skipped → ~0px rendered section tall thumb, jumps on scroll rendered section placeholder 800px placeholder 800px steady thumb, stable height reserve

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.

How the auto keyword learns real size On first render the browser measures actual height and stores it, then substitutes the remembered size on every later skip instead of the fallback estimate. skipped: uses fallback 800px first render: measure 1100px store last size 1100px remembered re-skip: reuses 1100px, no drift contain-intrinsic-size: auto 800px fallback only matters until the subtree renders once

Scroll Anchoring and CLS

Because content-visibility implies layout containment, each skipped subtree resizes in isolation, but the document above the anchor still reflows when its box changes. 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.

Placeholder-to-real delta becomes CLS above the anchor A skipped subtree above the scroll anchor grows from its placeholder height to its real height, pushing anchored content down and recording a layout shift. Before render (estimate) After render (real height) placeholder 800px anchored content scroll anchor here real 1180px +380px delta anchored content pushed down renders shift below anchor → CLS 0.18 (hadRecentInput: false)
/* 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.

Attributing a shift to a skipped subtree A layout-shift entry is filtered for hadRecentInput, then its sources array is walked to the node so the placeholder estimate can be corrected. layout-shift entry hadRecentInput? false → keep entry.sources walk nodes .chapter node fix estimate PerformanceObserver attribution path input-driven shifts are dropped before the source is blamed

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

Why the Estimate Travels With the Component

The failure mode that undermines contain-intrinsic-size in practice is drift: the value is set once against today’s typical content, then a redesign changes the section’s real height and nobody updates the estimate. When the placeholder and the real height diverge, the scrollbar starts drifting again and scroll-anchoring can misfire, because the browser reserved the wrong amount of space for skipped content. The discipline that prevents this is to treat the intrinsic-size value as data that belongs to the component, not a magic constant in a global stylesheet — colocate it with the component, derive it from a representative measurement, and revisit it when the component’s layout changes.

Modern engines soften the problem with contain-intrinsic-size: auto <value>, which remembers the last rendered size and reuses it as the placeholder once the element has been on screen. This makes the initial estimate matter only until first render, after which the real measured height takes over and scrolling back past the section does not shift. On engines without auto support the static estimate is all you have, so it pays to pick it from the 75th-percentile height of real content rather than the smallest example — reserving slightly too much space costs nothing visible, while reserving too little reintroduces the shift the property was meant to eliminate. The interaction with scroll anchoring, and how the browser keeps the viewport stable as skipped content resolves, is the crux of getting long virtualized pages to feel solid rather than jumpy.

Frequently Asked Questions

Does contain-intrinsic-size do anything without content-visibility?

On its own contain-intrinsic-size only supplies an intrinsic size for a box that has size containment (or content-visibility that establishes it). Without containment there is nothing to substitute a placeholder for, so the property is effectively inert. Pair it with content-visibility: auto on the same element.

Why keep a fallback value when I use the auto keyword?

The auto keyword only has a remembered size after the subtree has rendered at least once. The fallback in contain-intrinsic-size: auto 800px is what the browser uses for the very first paint, before any measurement exists — so choose it from a measured median height to minimize the initial shift.

Can a wrong placeholder estimate cause CLS even below the fold?

Scroll anchoring only compensates for changes above the scroll position, so a bad estimate matters most there. A subtree that grows below the current anchor still records a layout-shift entry when it renders into or near the viewport, which is why the median-based fallback matters for every skipped section, not just the ones above.

How do I pick a good fallback height?

Instrument real content: measure the rendered height of each skipped section across representative data, take the median, and use that as the fallback. Feeding the estimate from live measurement keeps the placeholder-to-real delta near zero, so even the first render of a section shifts little.