Rendering PipelineApr 19, 2026·7 min read·

CSS containment is the spec's performance escape hatch

Layout thrashing posts tell you what not to do. `contain: layout` tells the browser a subtree's effects won't escape — so they don't, and perf scales. Most senior FE devs have never shipped it. Here's the four-value contract, a blast-radius visualizer, and the two-line shorthands.

Pick a containment boundary and hit "mutate leaf". Watch the layout blast climb the tree — and then stop at the boundary you placed:

place a containment boundary
layout blast on leaf mutation
blast escapes to <main>
depth 0<main>
depth 1<article>
depth 2<section>
depth 3<div.row>
depth 4<article.card>
depth 5<p>target</p>mutation origin
Rose = re-layout triggered. Green = containment boundary. Slate = protected by containment above the boundary.

contain: layout is one of the highest-leverage CSS properties most senior FE devs have never shipped. It's a formal opt-in to performance isolation. You're making a promise to the browser: the contained subtree's layout won't leak outward, and the outside world's layout won't leak in. The browser accepts the promise and optimises around it. Every "avoid whole-tree reflow" guideline you've read is addressed, in one property, by the CSS Containment Level 2 spec. Shipped in every major browser since 2022.

This post walks the four containment values, reads the primary source for each, and ends with the two shorthands you actually type in real codebases.

tl;dr

CSS Containment defines four opt-in isolation properties: contain: size | layout | paint | style. Each one declares that a specific side-effect of the element's subtree will not escape, and the browser uses that guarantee to avoid unnecessary work. contain: layout stops layout cascades; contain: paint clips paint; contain: size lets the browser layout the box before descendants compute; contain: style scopes counters and quotes. The shorthands contain: content (layout + paint + style) and contain: strict (+ size) cover the common cases.

The four primitives

CSS Containment defines four independent axes. Each is a clause in the spec. Here's the full set, and what each normative sentence actually says:

"the containment box is totally opaque for layout purposes; nothing outside can affect its internal layout, and vice versa."
— CSS Containment Level 2 §4
isolates · Layout changes inside stay inside. Float escape, contain-anchor positioning boundary, fixed-position containment — all isolated.
Typical target for performance isolation on large data grids or complex widgets.
contain: content
layout paint style
The usual-suspects shorthand. Gets you performance isolation without requiring a known size.
contain: strict
size layout paint style
All-in. Requires you to declare the box's size explicitly. Maximum isolation, strictest constraints.

You don't want all four blindly. Start with layout and paint — those are the perf wins. Add size only when you can declare dimensions. Add style only when counters or quotes are relevant. The shorthands — content (layout + paint + style) and strict (+ size) — are the escape hatches for "I just want this contained and I know what I'm doing".

What contain: layout actually buys you

Per the spec:

Layout containment ensures that the containment box is totally opaque for layout purposes; nothing outside can affect its internal layout, and vice versa.

Concretely, that sentence is implemented as a set of invariants the browser is now allowed to assume:

  • Float intrusion stops at the boundary — a float in a sibling cannot shift a contained box's inline content.
  • Fixed and absolute descendants inside the contained box are positioned relative to the contained box's padding edge, not the nearest positioned ancestor further up.
  • The browser may skip re-layout of an entire contained subtree when something outside changed, because the subtree cannot have been affected by that change.
  • The browser may also skip re-layout of everything outside the contained subtree when something inside changed, because the inside can't have affected the outside.

The second bullet is the big performance win. Take a data grid with 2,000 rows, 12 cells each. Without containment, a DOM mutation on row 500 — a class toggle, a text update — is potentially a layout cascade up to <body>. With contain: layout on each row, the cascade stops at row 500's boundary. The browser re-lays-out that one row. The other 1,999 stay put, and neither does the grid container or anything outside it.

That's the blast-radius demo at the top of this post. Pick a boundary at <article> and mutating the leaf only re-lays-out from the leaf up to <article>. Without a boundary, the blast reaches <main>.

contain: paint is the cheapest invisibility rule

Paint containment has a two-fold spec:

Paint containment ensures that the descendants of the containment box don't display outside its bounds, so if an element is off-screen or otherwise not visible, its descendants are also guaranteed to be not visible.

The practical consequence: if a contained box is off-screen, the browser knows for certain its descendants paint nothing visible, so it can skip painting them. Without paint containment, a descendant might have position: fixed or a huge transform and poke outside the parent's bounds — the browser has to paint it conservatively.

The common pattern it unlocks: a virtualized list where items outside the viewport can be layout-computed but not paint-computed. With contain: paint on each row, off-screen rows are a zero-paint cost.

contain: size requires you to declare dimensions

Size containment is the strictest of the four and the one most senior devs trip on. Per the spec:

Size containment ensures that the containment box can be laid out without needing to examine its descendants.

The browser can only lay out a size-contained box if the box itself tells the browser its dimensions — width, height, aspect-ratio, or flex/grid-imposed sizing. If you apply contain: size without specifying a size, the contained element collapses to 0×0, because the browser is forbidden from looking at descendants to compute the size.

This sounds like a footgun, but it pairs beautifully with virtualization: a row whose height is declared can be contain: strict, and the browser knows the row's dimensions before it examines any descendant text or image. The trade-off is that the row must be the declared size; text that wraps differently than you expected is clipped or overflows.

content-visibility is the sugar for the common case

The spec introduced one more tool that leans on containment: content-visibility: auto. It's not a containment value. When an auto element is off-screen, the browser applies layout, style, and paint containment (not size — that's why contain-intrinsic-size supplies the placeholder height). When it scrolls near the viewport, containment is lifted and the contents render normally. One line of CSS gets you virtualization for free:

.list-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 120px; /* placeholder height */
}

Chromium, WebKit, and Firefox all support it. It's one of the cheapest real-world perf wins on a long list — the browser skips layout and paint for off-screen items until you scroll them into view.

When to reach for which

A short decision guide from reading the spec plus a few projects of real deployment:

  • Widgets, data grids, lists of cards. contain: content. Gets you layout + paint + style isolation without requiring you to declare sizes.
  • Virtualized rows / off-screen items. content-visibility: auto with contain-intrinsic-size placeholders. Lets the browser skip the whole subtree when it scrolls out of view.
  • Complex widgets where you fully control the size (popups, modals with known heights). contain: strict. Maximum isolation; requires dimension declarations.
  • Scope counters or quotes inside a widget. contain: style. Rarely used on its own.
  • Avoid applying containment to whole pages or top-level containers. The isolation buys nothing (nothing outside could have leaked in) and costs you flexibility if the page layout is still settling.

What containment is not

Two common misreadings worth pinning down:

  • Containment is not overflow: hidden. overflow: hidden clips the visual box to its content area, but doesn't stop layout cascades, doesn't change paint guarantees, doesn't isolate counters. They're orthogonal properties with superficially similar visual effect.
  • Containment is not will-change. will-change is a hint to the compositor for layer promotion. Containment is a guarantee from you to the browser about subtree behaviour. The compositor may ignore will-change; the layout engine cannot ignore contain. Containment is normative.

Primary sources