CSS Containment (contain)
Rendering a page is more than "apply styles once." When something changes — content updates, an element resizes — the browser has to figure out how far that change ripples: does it affect just this element, its subtree, or potentially the entire page? The contain property lets you tell the browser, explicitly, "whatever happens inside this box, stays inside this box," which lets it skip work outside the boundary entirely.
The four containment types
Value | What it isolates |
|---|---|
| The element becomes an independent layout root — its internal layout cannot affect, and is not affected by, layout outside it |
| Descendants can't paint outside the element's bounds; also implies clipping, similar to overflow: hidden for rendering purposes |
| The element's size is computed without looking at its children's content at all — must be paired with an explicit size |
| Certain style effects (like counters) are scoped so they cannot leak in or out of the element |
.widget {
contain: layout paint; /* the two most commonly combined together */
}
/* Shorthand for common combinations */
.widget-a {
contain: content; /* layout + paint + style */
}
.widget-b {
contain: strict; /* layout + paint + style + size (needs explicit size) */
}Why this matters for performance
Without containment, a layout change anywhere in the DOM can, in the worst case, force the browser to re-check layout for the entire page — because technically an ancestor's size could depend on any descendant. Containment lets the browser prove that a subtree is self-contained, so it only needs to redo layout/paint work inside that boundary.
/* Without containment: a single card resizing (e.g. new content loads)
could, in theory, ripple layout calculations through ancestors and
siblings, because nothing tells the browser otherwise. */
.card {
padding: 16px;
}
/* With containment: the browser can safely assume this card's internal
changes never affect anything outside its box. */
.card {
padding: 16px;
contain: layout paint;
}contain: size tells the browser to size the element as if it had no content at all — which means the element collapses to zero size unless you also give it an explicit width/height (or use aspect-ratio). It is rarely used alone for this reason; it usually shows up bundled inside content-visibility: auto instead.Relationship to content-visibility
content-visibility: auto is the practical, high-impact use of containment — it automatically applies layout, style, and paint containment to off-screen content, and additionally skips rendering that content's subtree entirely until it scrolls into view.
.long-article section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* placeholder size while off-screen */
}content-visibility: auto gets you the benefit of containment automatically, with less manual tuning than applying contain by hand everywhere. Use bare contain when you specifically need isolation without the "skip rendering off-screen" behavior.When browsers apply containment automatically
Certain elements get implicit containment from the spec, without any CSS — for example
contain: contentbehavior is implied on elements like<details>in some enginescontent-visibility: autoapplies containment automatically to any element it is set onExplicit
containis still the only way to get containment on an arbitrary element you control
Containment for widgets and components
Independent, self-contained UI widgets are the ideal candidate for containment — a chat widget, a map embed, a comment thread — anything whose internal changes genuinely have no business affecting the rest of the page.
.chat-widget {
contain: layout style paint;
/* Internal re-renders (new messages arriving, typing indicators)
never force the browser to re-check layout outside this box */
}contain: paint (and the shorthands that include it) clips overflowing content and establishes a new containing block for absolutely/fixed positioned descendants — the same side effects as overflow: hidden plus a positioning context. Test tooltips, dropdowns, and anything relying on escaping the box's bounds before applying it broadly.Measuring the benefit
Open DevTools Performance panel, record a scroll or an interaction that updates content
Compare the "Layout" and "Recalculate Style" time before and after adding
containOn a long list/page with many independent widgets, the difference tends to show up as far fewer, much smaller layout passes
contain vs will-change
Both properties are performance hints, and both create side effects (new stacking/containing contexts), but they solve different problems. will-change tells the browser to prepare for an upcoming change (usually ahead of an animation); contain tells the browser an element's internal changes never escape its box.
Property | What it optimizes | Typical use |
|---|---|---|
| Isolates layout/paint/style so changes inside never ripple outside | Independent widgets, list items, self-contained components |
| Pre-promotes an element to its own compositor layer ahead of an animation | An element about to animate |
Containment and tables
Table layout is one place where contain: layout has a visible, sometimes surprising effect: since a contained element becomes an independent layout root, applying containment to a table cell's content can change how intrinsic sizing interacts with the rest of the table.
.table-cell-content {
contain: layout;
/* This cell's content no longer influences sibling cell sizing in the
way it otherwise would -- verify actual table alignment after
adding containment to any cell content */
}A before/after measuring example
Build (or find) a page with many repeated, independent widgets — e.g. 50 dashboard cards, each with its own internal state
Open DevTools Performance panel, record while triggering an update to one card (a counter incrementing, a badge changing)
Note the "Recalculate Style" and "Layout" durations in the recorded trace
Add
contain: layout style paintto the card componentRepeat the same recorded interaction and compare the same two metrics
On a page with many siblings, the layout/style recalculation scope should shrink noticeably, since the browser can now prove the change cannot affect anything outside the card
Browser support
Value | Support level |
|---|---|
| Broadly supported in evergreen browsers |
| Broadly supported in evergreen browsers |
| Broadly supported, effect is narrower (mainly counters) |
| Broadly supported, but rarely useful on its own |
| Chromium and Firefox; historically slower in Safari |
contain value is simply ignored by older engines — the element renders exactly as it would without containment, just without the performance benefit. There is no visual fallback to design for, which makes it low-risk to adopt even before checking Baseline status carefully.Quick recipe for a typical component library
/* A sensible default for most independent, self-contained widgets:
layout + paint containment, without size (which needs an explicit
size and is easy to get wrong). */
.widget {
contain: layout paint;
}
/* Reach for content-visibility instead when the widget is also
frequently off-screen (long lists, feeds): */
.feed-item {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}