Layout Containment
The Problem: Global Layout
Here is a question that should scare you: when you change the height of a single card on your page, how much of the page does the browser recalculate? By default, potentially the entire thing. Adding a paragraph to a <div> can change the height of every subsequent sibling, the parent's scrollbar, and trigger a cascade of layout recalculations across the document. Layout is global — the browser must verify that nothing changed anywhere.
<main>
<section class="hero">...</section>
<section class="features">
<div class="card">This card's height changes</div>
<!-- Every sibling below recalculates position -->
</section>
<section class="pricing">...</section> <!-- Shifts down -->
<section class="testimonials">...</section> <!-- Shifts down -->
<footer>...</footer> <!-- Shifts down -->
</main>
A single card expanding forces the browser to recalculate layout for the entire document from that point down. On a page with 5,000 DOM nodes, this can take 10-30ms — an entire frame budget.
Imagine a spreadsheet where changing one cell recalculates the entire sheet. CSS containment is like telling the spreadsheet: "This range of cells is independent — changes inside it cannot affect cells outside." The spreadsheet engine can skip recalculating everything outside the contained range. The browser works the same way: contain creates boundaries that limit the scope of layout, paint, and style recalculations.
The contain Property
So how do you tell the browser "this section is independent, stop recalculating everything"? The contain property accepts one or more containment types:
.widget {
contain: layout; /* Layout changes inside don't affect outside */
contain: paint; /* Painting is clipped to this element's bounds */
contain: size; /* This element's size is independent of children */
contain: style; /* Counters and quotes scoped to this subtree */
/* Shorthands */
contain: content; /* = layout + paint + style (safe default) */
contain: strict; /* = layout + paint + size + style (maximum containment) */
}
Layout Containment
.card {
contain: layout;
}
Effects:
- The element acts as a containing block for absolutely/fixed positioned descendants
- Layout changes inside the element do not trigger layout recalculation outside
- The element establishes an independent formatting context (like
overflow: hiddenwithout clipping)
// Without contain: layout — layout scope is entire document
card.style.height = '500px'; // Browser recalculates layout for entire page
// With contain: layout — layout scope is limited to card's subtree
card.style.height = '500px'; // Browser recalculates only the card's contents
Paint Containment
.widget {
contain: paint;
}
Effects:
- Content that overflows the element's bounds is not painted (implicit clipping)
- The element acts as a containing block for absolutely positioned descendants
- The browser can skip painting this element entirely if it is off-screen
Basically, you are telling the browser: "Nothing inside this element will ever be visible outside its bounds." This lets the browser skip paint for off-screen contained elements entirely — it knows no descendant can poke out and be visible.
Size Containment
.widget {
contain: size;
width: 300px;
height: 200px;
}
Effects:
- The element's size is determined by its explicit width/height, ignoring children
- The browser does not need to lay out children to know this element's size
- If no explicit size is set, the element collapses to 0×0
contain: size without explicit dimensions collapses the element. This is the most common mistake with size containment — developers add contain: strict (which includes size) without setting width and height, and the element disappears. Always pair size containment with explicit dimensions.
The content and strict Shorthands
/* Safe for most use cases */
.card {
contain: content; /* layout + paint + style */
/* Children's content determines the card's size (no size containment) */
/* But layout/paint changes are scoped to this subtree */
}
/* Maximum containment — requires explicit sizing */
.fixed-widget {
contain: strict; /* layout + paint + size + style */
width: 300px;
height: 200px;
/* Browser knows everything about this element without looking at children */
}
content-visibility: auto
Now for the real powerhouse. content-visibility is containment's most impactful application — and honestly, it might be the single highest-ROI CSS property for long pages. It tells the browser to skip rendering for off-screen elements entirely:
.section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
When an element with content-visibility: auto is off-screen:
- No style computation for descendants
- No layout for descendants
- No paint for descendants
- The element retains its
contain-intrinsic-sizefor scroll height calculation
When the element scrolls into view, the browser renders it on demand.
contain-intrinsic-size
There is a catch, though. The browser still needs to know the element's size for scrollbar calculation, even when content is not rendered. contain-intrinsic-size provides that estimate:
.section {
content-visibility: auto;
/* Fixed estimate */
contain-intrinsic-size: 0 500px; /* width: auto, height: 500px estimate */
/* Auto-remembering: uses last rendered height, falls back to 500px */
contain-intrinsic-size: auto 500px;
}
The auto keyword tells the browser: "Use the actual rendered height if you've measured it before, otherwise use 500px." As the user scrolls, the browser learns each section's true height and stores it, making scrollbar position progressively more accurate.
Measuring Containment Impact
Performance Panel Comparison
// Without containment
console.time('no-containment');
for (let i = 0; i < 100; i++) {
items[0].style.height = (100 + i) + 'px';
// Force synchronous layout
document.body.offsetHeight;
}
console.timeEnd('no-containment');
// With containment
items.forEach(item => item.style.contain = 'content');
console.time('with-containment');
for (let i = 0; i < 100; i++) {
items[0].style.height = (100 + i) + 'px';
document.body.offsetHeight;
}
console.timeEnd('with-containment');
// Typical results on a page with 2000 DOM nodes:
// no-containment: 850ms
// with-containment: 120ms (7x faster)
content-visibility Impact
Google's case studies show:
- Blog with 50 articles: Initial rendering time reduced from 232ms to 30ms (7.7x faster)
- Infinite scroll feed: Scroll jank eliminated — off-screen items have zero rendering cost
- Documentation sites: Pages with 100+ headings render in under 50ms instead of 400ms+
How the browser determines 'off-screen'
For content-visibility: auto, the browser uses IntersectionObserver internally to determine when an element enters or exits the viewport (plus a margin). The default margin is implementation-dependent but typically extends beyond the viewport to start rendering before elements scroll into view. This prevents pop-in artifacts. You cannot customize this margin — it is managed by the browser's heuristics.
Practical Patterns
Long Scrollable Lists
.list-item {
content-visibility: auto;
contain-intrinsic-size: auto 80px; /* Estimated row height */
}
Tab Panels
.tab-panel {
contain: strict;
/* Tab panels have fixed dimensions */
}
.tab-panel[hidden] {
content-visibility: hidden;
/* Fully skip rendering for hidden tabs */
/* Unlike display:none, state is preserved (form inputs, scroll position) */
}
Complex Widgets
.dashboard-widget {
contain: content;
/* Each widget is independent — layout changes in one don't affect others */
}
content-visibility: hidden hides the element and skips all rendering, but unlike display: none, the element retains its internal state (form values, scroll positions, focus). It also retains its layout slot. Think of it as "offscreen rendering paused." Use it for tab panels and conditional content that should preserve state.
- 1CSS contain creates layout boundaries — changes inside a contained element do not trigger recalculation outside it.
- 2contain: content (layout + paint + style) is the safe default for most components.
- 3contain: strict (adds size) requires explicit dimensions or the element collapses to 0×0.
- 4content-visibility: auto skips style, layout, and paint for off-screen elements — the most impactful single optimization for long pages.
- 5Always pair content-visibility: auto with contain-intrinsic-size for correct scrollbar behavior.
- 6content-visibility: hidden preserves internal state (unlike display: none) while skipping all rendering work.
- 7Containment turns O(total DOM) layout into O(subtree) layout — the more complex your page, the bigger the win.
Q: Your documentation site has 200 sections on a single page. Initial render takes 800ms and scrolling janks. How would you use CSS containment to fix this?
A strong answer covers: apply content-visibility: auto with contain-intrinsic-size: auto <estimated-height> to each section. This skips rendering for the ~190 off-screen sections on initial load, reducing render time from O(200 sections) to O(~10 visible sections). For scrolling, contained sections limit layout recalculation scope. Mention that contain-intrinsic-size: auto remembers previously measured heights for accurate scrollbar sizing. Bonus: use the Performance panel to measure before/after layout times, check that the auto margin is sufficient to prevent pop-in, and note that content-visibility is supported in Chrome, Edge, and Firefox (Safari added support in 17.x).