CSS Containment
contain is a promise you make to the engine — nothing inside this box affects anything outside it — and in exchange, invalidation stops at the boundary.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
How do I tell the browser that a subtree cannot affect the rest of the page, and what am I giving up by saying so?
A person is using a page built from many independent pieces — a feed, a dashboard, a table of cards — and expects updating one of them to be as fast as if the others were not there.
Add contain: strict to every component root. It is a performance hint, hints are free, and the browser will use it if it helps.
It is not a hint. It is a declaration with defined effects, and the engine is entitled to act on it — including sizing your element as if it had no content, which is what size containment means and why strict so often produces an invisible box.
- It is not a hint. It is a declaration with defined effects, and the engine is entitled to act on it — including sizing your element as if it had no content, which is what
sizecontainment means and whystrictso often produces an invisible box. - Paint containment clips to the padding box. Dropdowns, tooltips, focus rings and hover cards that used to escape the component are now cut off, and the CSS that broke them is nowhere near them.
- Layout containment makes the element a containing block for absolutely and fixed-positioned descendants. A
position: fixedoverlay inside a contained subtree stops being fixed to the viewport (Positioning and Stacking Contexts). - Style containment scopes counters and quotes, so numbered lists and nested quotation marks restart in ways nobody asked for.
- Containment on hundreds of elements is not free — each boundary is bookkeeping, and paint containment creates a stacking context that can encourage layer promotion you did not want (Layer Explosion).
What is actually happening
In the browser, not in the framework.
- Layout containment promises that the internal layout of the box cannot affect anything outside it and vice versa. The element establishes an independent formatting context, becomes a containing block for positioned descendants, and creates a stacking context.
- Paint containment promises that descendants will not paint outside the box's padding box — so the engine clips there, creates a stacking context, and can skip painting the subtree entirely when the box is off screen.
- Size containment promises that the element's own size does not depend on its contents. The engine sizes it as if it were empty, which is why it collapses unless you give it a size — or an intrinsic size to use as a placeholder.
- Style containment promises that effects which can escape a subtree — counters, quotes — are scoped to it.
contain: contentis layout plus paint plus style: the useful default, because it bounds propagation without touching sizing.contain: strictadds size, which is the dangerous one.- Containment does not make anything faster by itself. It shrinks the set of elements a stage must consider — layout inside a contained subtree cannot dirty layout outside it, and paint inside cannot dirty pixels outside — which is what makes the invalidation cheaper (Style Invalidation).
- Container queries build on the same machinery: declaring a container applies the containment its query axis requires, which is why a size container behaves like a contained box in ways that surprise people (Container Queries).
What this makes the browser do
And which of it is avoidable.
- Maintaining the boundary: recording that layout for this subtree can be resolved independently, and that invalidation does not propagate across it.
- Skipping layout for contained subtrees whose inputs did not change, even when an ancestor was laid out again.
- Skipping paint for contained subtrees that are entirely outside the visible region — the direct ancestor of what
content-visibilityautomates (content-visibility). - Establishing an extra formatting context and stacking context per contained element, which has its own cost and changes paint order.
- Avoidable work removed: relayout of every card in a feed because one of them changed height. Work added: boundary bookkeeping on every contained element, whether or not it ever pays off.
What each keyword promises
Every value of contain is a specific promise about what cannot escape the box, and each promise buys the engine a specific permission to skip work. Reading them as promises rather than as flags is what makes the failures predictable: if the promise is false for your component, the resulting bug is the promise being enforced, not the browser misbehaving.
The right-hand column is the one to read carefully before shipping. Every one of these changes something about how your box behaves, and the change is not scoped to the times when the optimisation pays off.
| Value | You promise | The engine may skip | What changes about your box |
|---|---|---|---|
layout | Internal layout cannot affect anything outside, and outside layout cannot reach in | Laying out this subtree when an ancestor relayouts for unrelated reasons | Becomes an independent formatting context, a containing block for absolutely and fixed positioned descendants, and a stacking context |
paint | Descendants do not paint outside the padding box | Painting the subtree at all when the box is off screen | Clips overflow at the padding box and creates a stacking context — tooltips, menus and focus rings are cut off |
size | The box's own size does not depend on its contents | Laying out descendants in order to size the box | Sized as if empty: collapses to zero in the contained axis unless you give it a size or a contain-intrinsic-size |
style | Effects that can escape a subtree — counters, quotes — stay inside it | Tracking those effects document-wide | Counters and quotes are scoped, so counter-increment inside restarts rather than continuing |
inline-size | The inline-axis size does not depend on contents | Inline-axis sizing work that would depend on descendants | Single-axis size containment; the substrate for inline-axis container queries (Container Queries) |
content | Layout, paint and style containment together | Most propagation across the boundary, without touching sizing | The sensible default: clips overflow and changes containing blocks, but the box still sizes to its content |
strict | All four, including size | The most — including laying out the subtree at all when it is off screen | Everything above, plus the collapse. Almost always wants a contain-intrinsic-size alongside it |
The boundary, and what it actually stops
Layout propagates in both directions by default. A child that grows can force its parent to resize, which can move the parent's siblings, which can change the height of the page. A parent that changes width can change the available space for every descendant. Containment cuts one or both of those directions at a declared point.
That is the whole benefit, and it is why the shape of the page matters more than the number of elements. On a feed of independent cards, one card changing height would otherwise dirty the geometry of everything below it; with the boundary in place, the layout that runs is the card's own. On a page where nothing was propagating anyway, the same declaration removes capabilities and saves nothing.
Size containment is the one that bites
Three of the four containment types cost you an edge case. Size containment costs you the element. The promise is that the box's size does not depend on its contents, so the engine sizes it as if there were no contents — and for anything without an explicit height, that means zero.
This is not a trap the browser sets; it is the promise being kept. The fix is to keep the promise honestly: give the box a size, or give it an intrinsic size to use as a stand-in when it is not laying its contents out. contain-intrinsic-size exists for exactly this, and its auto form lets the engine remember the size the element had the last time it was rendered.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
contain: strict added to a component root | The component renders as a zero-height sliver; content is in the DOM and invisible | Size containment means the box is sized as if empty | Use contain: content, or keep strict and supply contain-intrinsic-size (content-visibility). |
| Paint containment on a card containing a dropdown | The menu is clipped at the card edge, worst for cards near the bottom of the list | Paint containment clips descendants to the padding box | Render the menu outside the contained subtree, or drop paint containment for that component. |
A position: fixed modal inside a contained ancestor | The overlay positions against the card instead of the viewport, and scrolls with it | Layout containment makes the element a containing block for fixed descendants | Move the overlay to a container outside the boundary — this is what portals are for (Positioning and Stacking Contexts). |
Style containment on <li> elements | Ordered list numbering restarts at every item | Counters are scoped to the containment boundary | Do not include style in the containment for elements participating in document-wide counters; use layout paint explicitly. |
| Containment applied broadly "for performance" | No measurable change; several unrelated visual bugs appear over the next weeks | The page was not layout-bound, and every boundary removed a capability | Profile first, apply at the boundaries the propagation actually crosses, and remove containment nobody can attribute a win to. |
| A win measured in one browser | The same change shows no improvement in another engine's trace | How much work is skipped for a given promise is an engine decision | Measure per engine; keep the change only if the correctness cost is acceptable regardless of the win. |
1/* Safe default: bounds propagation, box still sizes to content. */2.card {3 contain: content; /* = layout paint style */4}5 6/* Dangerous without a size: the engine sizes this as if it were empty. */7.card--strict {8 contain: strict; /* = size layout paint style */9 /* ...so the card collapses to zero height. */10}11 12/* Keeping the promise honestly: supply the size the engine may not derive. */13.card--strict {14 contain: strict;15 contain-intrinsic-size: auto 220px;16 /* `auto` remembers the last rendered size; 220px is the fallback17 used before this card has ever been laid out. */18}19 20/* Paint containment clips at the padding box — this menu is now cut off. */21.card .menu { position: absolute; top: 100%; }The last two lines are the failure that survives review: the CSS that clips the menu is on .card, and the symptom appears on .menu, in a component someone else owns.
How to build it
Most important first.
- Reach for
contain: contentfirst. It captures most of the benefit and leaves sizing alone, which is where the failures come from. - Apply containment where the DOM already has a boundary — a list item, a card, a widget root — rather than sprinkling it. If the component can legitimately affect its surroundings, the promise is false and the resulting bug is yours.
- Use
sizecontainment only when you also supply the size, either explicitly or throughcontain-intrinsic-size. Treat "I addedstrictand it disappeared" as the expected outcome, not a browser bug. - Check overflow before adding paint containment. Anything that must escape the box — menus, tooltips, custom selects — has to be portalled out or repositioned, which is an architecture change and not a CSS tweak.
- Measure the layout time you are trying to remove first. Containment on a page whose cost is style recalculation or paint changes nothing, and you will have paid its correctness risk for no benefit (Measure Before Optimising).
- Prefer it for the repeated-independent-subtree shape: feeds, tables, grids of cards. That is where the propagation it blocks is both real and large.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Containment does not remove anything from the accessibility tree — and that is exactly the trap. Content clipped away by paint containment is still exposed, still focusable and still in the tab order, so keyboard focus can land on something the user cannot see (Focus Management).
- A box collapsed by size containment has the same problem in a worse form: a screen reader will read content that occupies no space on screen, and a sighted keyboard user will lose the focus ring entirely (Keyboard Operability).
- Focus rings are painted, so paint containment clips them at the boundary. An element flush against the edge of a contained box can appear to have no focus indicator at all.
- Find-in-page still matches text inside contained subtrees, and the browser will try to scroll to it — into a box that may be clipping it. Containment changes what is visible, never what exists.
- If content genuinely should not be perceivable, remove it from the tree —
display: none,hidden, or not rendering it — rather than relying on a clip to hide it (The Accessibility Tree).
What can go wrong
- The collapsed box:
contain: strictorcontain: sizewith no size supplied, producing a zero-height element whose content is present in the DOM and invisible on screen. - The clipped menu: paint containment on a card, and the dropdown inside it is cut at the card's edge on exactly the rows near the bottom of the viewport.
- The unfixed overlay: a modal using
position: fixedinside a layout-contained ancestor, now positioned against the card rather than the viewport. - The restarted counter: style containment applied to list items, and the ordered list numbering resets on every item.
- The mitigation failing: adding containment to reduce layout cost on a page that was paint-bound, so the trace looks identical and the dropdowns are now broken.
- Silent divergence between engines: containment is honoured everywhere, but *how much* work each engine actually skips differs, so the win measured in one browser may not appear in another.
- Containment is not an isolation boundary. It bounds rendering effects and nothing else: script inside a contained subtree has the same access to the whole document as any other script on the page (The Browser Security Model).
- Clipping is not hiding. Anything inside a contained box is still in the DOM, still readable by any script, and still present in the serialised HTML — never use it to keep something from a user (What the Frontend Is Responsible For in Auth).
- Third-party embeds are the case people reach for containment on, and the real boundary there is an iframe with an appropriate sandbox and origin, not a
containdeclaration (Third-Party Scripts and the Supply Chain). - Untrusted content that renders into a contained subtree can no longer break the page layout outside it, which is a genuine robustness benefit — but it is a defence against accidental damage, not against an attacker.
- "
containis a hint likewill-change."will-changeis advisory;containhas specified effects on layout, painting and sizing, and the browser applies them whether or not it makes anything faster. - "
contain: strictis the best one because it contains the most." It contains the most, including size, which is the one that makes elements vanish.contentis the sensible default. - "Containment makes rendering faster." It makes invalidation narrower. If the page was never re-laying-out the contained subtrees, it saves nothing.
- "It is just a performance feature, so it cannot break anything." It changes containing blocks, formatting contexts, stacking contexts and overflow behaviour. It is a layout feature that happens to have performance consequences.
- "Containment isolates a component." It isolates rendering effects. Scripts, styles from the outer document, and everything about the security model reach in exactly as before.
Measuring it, and what changes in the field
- Compare layout event duration and scope in a trace before and after. Containment either shows up as less layout work for the same interaction, or it did nothing and should be removed (Debugging Rendering and Jank).
- Look for layout events whose scope covers far more of the page than the change did — that pattern is what containment is for, and its absence means containment is the wrong tool here.
- Watch for the correctness signal in the same session: clipped overflow and collapsed boxes are visual, immediate and easy to miss in a headless check (Visual Regression Testing).
- Test with the keyboard after adding paint containment. Tab through the component and confirm every focus ring is visible; this catches the clipping bug faster than any screenshot diff.
- The benefit scales with how much propagation there was to block: a feed of a thousand independent cards is the ideal case, a page with six sections is not worth the risk.
- On a large DOM, containment converts a layout cost that grew with the page into one that grows with the changed subtree, which is the difference between an interaction that degrades over a session and one that does not.
- On a slow device the saved layout is proportionally more valuable, and so is the saved paint for off-screen contained subtrees.
- With dynamic content whose height is unknown, size containment is close to unusable without an intrinsic size, and a wrong intrinsic size produces layout shift instead of layout cost (Visual Stability).
- You trade correctness surface for pipeline savings. Every containment type removes a capability the box previously had, and the removed capability is often used by a component you have not looked at.
contain: contentis the safe subset, and it forgoes the largest single win — skipping the layout of off-screen subtrees entirely, which needs size containment orcontent-visibility.- Containment couples your CSS to your component boundaries. When a component grows a tooltip that must escape, the fix is architectural, not a one-line style change.
- The savings are real but invisible in code review: nothing about
contain: contentsays how much layout it prevented, so it accretes in codebases as a habit long after anyone measured it.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe
containproperty and the meaning of each value are specified and interoperable across Blink, Gecko and WebKit: the effects on containing blocks, formatting contexts, stacking contexts and overflow are the same everywhere, because they are defined behaviour rather than optimisations. - ENGINE-SPECIFICHow much work each engine actually skips given the same promise differs, and none of them publish a guarantee: a containment boundary that measurably removes layout time in one browser may show a much smaller effect in another on the same page. Measure the win per engine; the correctness effects are the only portable part.
- SPEC-EVOLVINGThe containment family has grown over time —
contentandstrictshorthands, theinline-sizevalue, and its use as the substrate for container queries — and support for the newer members lands at different times per engine. Feature-detect rather than assuming the whole family is available.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — containment is an encapsulation boundary expressed in CSS, and it fails in the same way every encapsulation boundary fails: when the abstraction promises something the implementation genuinely needs to violate.