The CSS contain Property: Isolating Layout for Performance Wins
On a page with thousands of DOM nodes, a single style change can ripple outward and cost real, measurable render time. Containment scopes that recalculation to one isolated box instead.
Paul Radford — Full-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

The contain property tells the browser that a specific subtree of the DOM is self-contained, so changes inside it will not affect layout, paint, size, or style outside its boundaries. This lets the rendering engine skip recalculating large portions of the page, which is the main source of the performance gain.
What Problem Is Containment Actually Solving?
Browsers are conservative by default. When something changes inside an element, the layout engine often has to check whether that change ripples outward: does a taller child force the parent to grow, does a new paint area bleed into a sibling, does a style change cascade to elements far away in the tree. On a small page this checking is invisible. On a page with thousands of DOM nodes (a data table, a chat feed, a dashboard with many widgets), that ripple-checking becomes real, measurable cost, often visible as long tasks in the Performance panel.
contain is a promise you make to the browser: "whatever happens inside this box, stays inside this box." The browser can then scope its recalculation work to that box alone. This is the same idea behind will-change, but contain is not a hint, it is a hard constraint that changes actual behavior (for example, it creates a new formatting context and can clip overflow), so you need to understand the side effects before applying it broadly.
The property is defined in the CSS Containment specification, and the practical explainer on MDN's contain page is the best day-to-day reference.
The Containment Types, and What Each One Actually Locks Down
contain accepts one or more of these values, and mixing them changes behavior in ways that are easy to get wrong:
- size: the element's size does not depend on its children. You must give it an explicit height (or it collapses to zero content height in some contexts), because the browser is no longer allowed to measure children to determine size.
- layout: the element becomes a layout containment boundary. Floats, absolute positioning, and margin collapsing inside it cannot escape or interact with the outside.
- paint: anything painted inside the box is clipped to its bounds. Nothing inside can paint outside the element's border box, similar to overflow: hidden but stronger, and it also means the browser can skip painting the subtree entirely if it is offscreen.
- style: counters and quotes defined inside do not leak out (this value has narrower browser support and is less commonly used in production).
- content: shorthand for layout paint style.
- strict: shorthand for size layout paint style.
In practice, most real-world usage boils down to two patterns: contain: content for "this widget is isolated but I still want its natural size to affect the page," and contain: strict for "this box has a fixed size and is fully sealed off."
1/* A dashboard widget that reflows independently of the rest of the page.2 Its internal layout changes (resizing charts, adding rows) will not3 force a layout recalculation of sibling widgets or the page shell. */4.dashboard-widget {5 contain: content;6 overflow: auto;7}89/* A fixed-size card in a virtualized list. Because we already know the10 height, we can use strict containment for the maximum performance11 benefit, including skipping paint work when scrolled offscreen. */12.list-row {13 height: 72px;14 contain: strict;15}Should You Just Add contain: strict to Everything?
No. This is the most common mistake once developers learn the property exists. size containment requires you to already know (or be willing to fix) the element's dimensions, and if you get that wrong, content overlaps or collapses in ways that are genuinely confusing to debug, because the visual bug and the CSS rule causing it are not obviously connected.
There are a few concrete cases where containment backfires:
Sticky and fixed positioning inside a contained ancestor. contain: layout (and by extension content and strict) establishes a new containing block for descendants, similar to how transform or filter do. If you have a position: sticky or position: fixed element nested inside a contained container, it may stick relative to the contained box instead of the viewport, which is rarely what you want. Test this explicitly if your component tree uses sticky headers.
Tooltips, dropdowns, and popovers that need to escape their container. paint containment clips overflow to the border box. A dropdown menu that visually needs to render outside its parent card will get clipped if that card has paint or strict containment. This is functionally similar to the classic overflow: hidden trap, except it is less obvious in the CSS because nobody wrote overflow: hidden on purpose.
Auto-sizing components with unpredictable content. If a card's height depends on user-generated text of unknown length, size containment forces you to either set a fixed height (losing flexibility) or use contain-intrinsic-size as a placeholder value, which can cause a visible jump when the real content measures differently. This is exactly the use case the content-visibility property was designed to solve more gracefully (see below).
contain vs content-visibility: Which One Do You Actually Want?
This is the decision point that trips up most people who learn about contain for performance reasons. They are related but solve different problems.
contain scopes recalculation. It does not skip rendering work, it just prevents that work from spreading outside the box. An off-screen element with contain: strict still gets its layout and paint computed in full, the browser just does not need to check whether that work affects anything outside the box.
content-visibility: auto, defined alongside containment in the same spec family and documented on MDN's content-visibility page, goes further: it skips layout and paint entirely for content that is not near the viewport, and it implies contain: layout style paint automatically. This is the property you want for long pages with many offscreen sections (documentation pages, long articles with embedded widgets, infinite-scroll feeds where you are not doing manual virtualization).
The practical rule I use: reach for contain alone when you have isolated, always-visible or frequently-updating widgets where you want to stop layout thrashing from spreading (charts, live-updating tables, form sections that resize). Reach for content-visibility: auto when you have a lot of content that is mostly offscreen and you want the browser to skip work on it until it scrolls into view. They compose fine together, but content-visibility is the bigger lever for scroll-heavy pages, and contain alone is the more surgical tool for interaction-heavy widgets that are always visible.
A Practical Example: Isolating a Live-Updating Widget
Here is a case that comes up constantly in dashboards: a widget that updates frequently (a price ticker, a live chart, a notification badge count) sitting next to static content. Without containment, every update can trigger a layout check against the entire page.
1<!-- Structure: a live-updating widget next to static sidebar content.2 Without contain, updates to .price-ticker can trigger layout3 recalculation checks against .sidebar and beyond. -->4<div class="page">5 <main class="price-ticker">6 <!-- numbers update every second via JS -->7 </main>8 <aside class="sidebar">9 <!-- static navigation, unrelated to the ticker -->10 </aside>11</div>1/* Isolate the ticker so its frequent updates don't cause the browser2 to re-check layout dependencies against the sidebar or page shell. */3.price-ticker {4 contain: content;5 /* content = layout + paint + style, but size stays flexible6 since we don't want to hardcode the widget's height */7}You can verify the effect using the Chrome DevTools Performance panel: record a trace while the ticker updates, and look at the "Layout" and "Recalculate Style" entries in the flame chart. Without containment, you will often see these entries scoped to a large portion of the render tree. With contain: content applied, the same operations should show a narrower scope, and on a sufficiently complex page the total time spent in layout during those updates drops measurably. This is also visible in the Layout Shift and Rendering panels if you enable "Layout Shift Regions" and "Paint flashing" to see exactly which boxes are being repainted.
Checking Support Before You Ship It
Basic contain (with layout, paint, size, content, strict values) has been supported in all major browsers since 2019 to 2021 depending on the engine (Chrome and Edge since version 52 for early values, with full support solidifying around Chrome 83, Firefox 69, and Safari 15.4). content-visibility is newer and Safari support arrived later than Chrome and Firefox, so check caniuse for your specific target browsers before relying on it for a critical rendering path. If you support older Safari versions, treat content-visibility as a progressive enhancement rather than a load-bearing performance strategy, and fall back to plain contain or manual virtualization for those users.
The safest rollout pattern is to apply containment to one widget type at a time, verify visually (checking for clipped tooltips, misbehaving sticky elements, and unexpected auto-sizing), and only then measure the performance delta with a real trace instead of assuming the win based on the spec description alone.
Related Articles

Optimizing LCP with the HTML fetchpriority attribute
Not every image on a page deserves the same loading priority from the browser's default heuristics. Here's when to mark the hero image high, and when deprioritizing the rest helps LCP more.

The content-visibility Property: Performance Gains for Long Pages
Virtualization libraries unmount off-screen DOM nodes to save render cost, but that breaks Ctrl+F and anchor links. This CSS property defers the rendering cost while keeping both intact.

Animating to height auto using CSS interpolate-size
height: auto was never a number the browser could interpolate toward, which is exactly why it never animated at all. These two features finally give the engine something concrete to target.