Web Platform · CSS Layout
Containment and Container Queries
What each containment type isolates, why size containment is required for query containers, and how query units and content-visibility change the sizing of a subtree.
A component that lays out differently above and below 400 pixels has to know its own width. Before container queries, the only reliable source for that number was the viewport, so responsive components were written against media queries and became correct only when they occupied the full width of the page. Container queries close that gap, but they do so by making a structural demand: the queried ancestor must be sized independently of its contents, or the query would be circular. That demand is what size containment enforces.
Containment is therefore not a rendering optimisation bolted onto layout. It is a set of restrictions on what a subtree can affect, and each restriction buys a guarantee that something outside the subtree can be computed without looking inside.
The four containment types
contain accepts size, layout, paint, style, and the combinations strict (all four) and content (layout, paint, style).
Layout containment makes the element an independent formatting context and the containing block for absolutely and fixed positioned descendants. It prevents floats and margins from escaping, and it means the subtree’s internal layout cannot affect the layout of boxes outside. The side effect that matters is the containing block change: a position: fixed descendant inside a layout-contained ancestor is positioned relative to that ancestor rather than the viewport.
Paint containment clips the element’s contents to its padding box and creates a stacking context, as described in the stacking order rules. It also makes the element a containing block for fixed descendants. Overflow that would have painted outside is simply not visible; the element does not become a scroll container, which is the difference from overflow: hidden.
Size containment treats the element as having no contents for the purpose of sizing. Its intrinsic sizes become zero, and its width and height must come from its own properties or from its containing block. A size-contained box with height: auto has a content height of zero unless a height is specified or the box is stretched by a flex or grid parent.
Style containment scopes counters and quotes so that changes inside the subtree cannot affect the rest of the document. Its visible effect is limited, but it is part of strict and content.
contain: strict applies all four; contain: content omits size containment, which is why the latter is safe on boxes whose height is driven by their content.
Why query containers need size containment
A container query asks whether a container is at least a given size and then styles its descendants. If the descendant’s style could change the container’s size, the query could change its own answer, and the layout would have no fixed point. The specification breaks the cycle by requiring size containment in the axis being queried.
container-type: inline-size applies size containment only in the inline axis. The container’s inline size is then determined without consulting its contents, typically by the block-level rules from its containing block, and the block size may still be content-driven. container-type: size applies containment in both axes, which requires a definite block size from somewhere else; without one, the container collapses to zero height.
The inline axis is the common choice because block-level boxes already take their inline size from their containing block. A card with container-type: inline-size still grows vertically to fit its content and can be queried on width. A card with container-type: size needs an explicit height, a grid stretch, or an absolutely positioned inset to have a height at all.
Containers are named with container-name, and queries select them by name. An unnamed container is still available to @container rules that do not name one; the nearest ancestor container in the relevant axis is used. Naming prevents a nested component from accidentally querying an outer card when it should query its own wrapper.
.card {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 30rem) {
.card__body {
display: grid;
grid-template-columns: 12rem 1fr;
}
}
/* Query units resolve against the nearest container in that axis. */
.card__title {
font-size: clamp(1rem, 4cqi, 2rem);
}
Query units and what they measure
Container query length units are percentages of the query container’s size. cqw is 1% of the container’s width, cqh 1% of its height, cqi 1% of its inline size, cqb 1% of its block size, and cqmin and cqmax the smaller and larger of the two. The units resolve against the nearest ancestor that is a query container for the relevant axis, not against the nearest ancestor with any container-type.
This is a meaningful difference from viewport units. 4cqi in a card that is 500 pixels wide is 20 pixels regardless of the browser window, while 4vw tracks the window. Components sized in query units scale with their slot in the layout and stay legible when the slot changes width, which is the property that makes them useful for typography inside components.
A unit with no eligible container resolves against the small viewport size, which is a fallback that can silently produce viewport-scale text in a component that was supposed to be container-relative. Defining a container on a wrapper is the fix.
content-visibility and skipped subtrees
content-visibility: auto allows the browser to skip layout and painting for an element’s contents while it is off-screen. The element itself still participates in layout, using the intrinsic size supplied by contain-intrinsic-size when the contents are skipped. On-screen contents are laid out normally.
The property implies contain: layout style paint on the element, and adds size containment while its contents are skipped. A skipped subtree cannot contribute to the intrinsic size of its ancestors, which is why contain-intrinsic-size accepts a width, a height, or both, and why leaving it unset makes off-screen elements collapse to zero and jump when they come into view.
The sizing substitution is observable. An element in a content-sized grid track with content-visibility: auto contributes the placeholder intrinsic size rather than its real one, so track sizes can change as contents become visible. For lists and long documents the trade is deliberate: layout work is deferred, and the size estimate is approximate.
Where containment changes behaviour
Each containment type has consequences beyond performance.
Fixed positioning breaks inside layout or paint containment. A fixed header inside a contained card is positioned against the card, and a modal opened inside a transformed or contained component is confined to it. Portalling such content to the document body, or moving the containment boundary above the modal, is the usual remedy.
Sticky positioning breaks inside size-contained boxes. A sticky element needs a scroll container and a containing block with room in the scroll direction. Size containment can remove the content height that the sticky range depends on.
Paint containment clips ink at the padding edge. The specification clips the element’s contents, including their ink overflow, to the padding edge, so a child whose shadow, outline or decorative element extends past the container is simply not painted beyond it. Nothing is created to reach the clipped part; a scroll container has to be reintroduced with overflow.
Style containment is not free of counters. Because counters are scoped, a counter incremented inside a contained subtree cannot be read outside it, which changes list numbering in nested components.
contain: size forces explicit sizing. A grid item with contain: size and align-self: stretch gets its block size from the track; a block-level box with contain: size and height: auto collapses. Both are correct per the specification and both surprise anyone who expects the content to size the box.
The alternative to containment-based queries is to measure elements in script and set classes, which reintroduces the observation loop that the layout engine manages internally and cannot avoid the first-paint delay that scripted measurement creates. The containment model exists so that the query can be answered during layout without a second pass, at the price of explicit sizing for the queried axis.