PipelineGENERALENGINE-SPECIFICSPEC-EVOLVING

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.

The question

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?

The user intent

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.

The obvious build

Add contain: strict to every component root. It is a performance hint, hints are free, and the browser will use it if it helps.

Why it breaks

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.

How it breaks in a real browser
  • 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.
  • 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: fixed overlay 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).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

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: content is layout plus paint plus style: the useful default, because it bounds propagation without touching sizing. contain: strict adds 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-visibility automates (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.

ValueYou promiseThe engine may skipWhat changes about your box
layoutInternal layout cannot affect anything outside, and outside layout cannot reach inLaying out this subtree when an ancestor relayouts for unrelated reasonsBecomes an independent formatting context, a containing block for absolutely and fixed positioned descendants, and a stacking context
paintDescendants do not paint outside the padding boxPainting the subtree at all when the box is off screenClips overflow at the padding box and creates a stacking context — tooltips, menus and focus rings are cut off
sizeThe box's own size does not depend on its contentsLaying out descendants in order to size the boxSized as if empty: collapses to zero in the contained axis unless you give it a size or a contain-intrinsic-size
styleEffects that can escape a subtree — counters, quotes — stay inside itTracking those effects document-wideCounters and quotes are scoped, so counter-increment inside restarts rather than continuing
inline-sizeThe inline-axis size does not depend on contentsInline-axis sizing work that would depend on descendantsSingle-axis size containment; the substrate for inline-axis container queries (Container Queries)
contentLayout, paint and style containment togetherMost propagation across the boundary, without touching sizingThe sensible default: clips overflow and changes containing blocks, but the box still sizes to its content
strictAll four, including sizeThe most — including laying out the subtree at all when it is off screenEverything above, plus the collapse. Almost always wants a contain-intrinsic-size alongside it

The boundary, and what it actually stops

ENGINE-SPECIFICThe diagram describes the permission containment grants, not a guarantee any engine takes it: Blink, Gecko and WebKit each decide independently how much layout to actually skip for a contained subtree, and the measured saving for identical markup can differ substantially between them. The behavioural changes — containing block, formatting context, clipping — are the interoperable part.

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.

Where invalidation stops
dirtycannot escape upwardnever dirtiednever dirtiedpage — layout runs here only if the feed's size changedfeed (contained: size not content-driven)card 2 — contain: content (changed)card 2 internals: relayout happens herepropagation stops at the boundarycard 1 — contain: contentcard 3 — contain: content
UserLLMAgentToolDataDecisionHumanGuardrail

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.

Containment bugs and what they actually are
TriggerSymptomCauseResponse
contain: strict added to a component rootThe component renders as a zero-height sliver; content is in the DOM and invisibleSize containment means the box is sized as if emptyUse contain: content, or keep strict and supply contain-intrinsic-size (content-visibility).
Paint containment on a card containing a dropdownThe menu is clipped at the card edge, worst for cards near the bottom of the listPaint containment clips descendants to the padding boxRender the menu outside the contained subtree, or drop paint containment for that component.
A position: fixed modal inside a contained ancestorThe overlay positions against the card instead of the viewport, and scrolls with itLayout containment makes the element a containing block for fixed descendantsMove the overlay to a container outside the boundary — this is what portals are for (Positioning and Stacking Contexts).
Style containment on <li> elementsOrdered list numbering restarts at every itemCounters are scoped to the containment boundaryDo 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 weeksThe page was not layout-bound, and every boundary removed a capabilityProfile first, apply at the boundaries the propagation actually crosses, and remove containment nobody can attribute a win to.
A win measured in one browserThe same change shows no improvement in another engine's traceHow much work is skipped for a given promise is an engine decisionMeasure per engine; keep the change only if the correctness cost is acceptable regardless of the win.
Containment on a feed of cards, with and without the collapse
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 fallback
17 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: content first. 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 size containment only when you also supply the size, either explicitly or through contain-intrinsic-size. Treat "I added strict and 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

Failure modes
  • The collapsed box: contain: strict or contain: size with 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: fixed inside 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.
Security
  • 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 contain declaration (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.
Misreads
  • "contain is a hint like will-change." will-change is advisory; contain has specified effects on layout, painting and sizing, and the browser applies them whether or not it makes anything faster.
  • "contain: strict is the best one because it contains the most." It contains the most, including size, which is the one that makes elements vanish. content is 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

How you would see this
  • 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.
Slow device, slow network, large data, old tab
  • 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).
What this costs
  • 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: content is the safe subset, and it forgoes the largest single win — skipping the layout of off-screen subtrees entirely, which needs size containment or content-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: content says 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 contain property 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 — content and strict shorthands, the inline-size value, 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.

Securitysandboxing
Computer Architectureworking-set
Domains that do not exist yet
  • 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.