Container Queries
A component should respond to the space it was given, not to the viewport it happens to be inside — which is why container-type establishes containment, and why that containment has real layout consequences.
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 same card is in a wide main column and a narrow sidebar. How does it know which one it is in?
A person sees the same card twice on one page — once large in the feed, once compact in a sidebar. They expect both to be readable and neither to be a squashed version of the other.
The sidebar only exists on wide viewports, so @media (min-width: 1024px) can style the sidebar copy. Failing that, add a --compact modifier class and let whoever places the component pass it.
The media query is answering a question about the page, and the component asked a question about itself. At 1024px the main column is wide *and* the sidebar is narrow, and one rule is applied to both.
- The media query is answering a question about the page, and the component asked a question about itself. At 1024px the main column is wide *and* the sidebar is narrow, and one rule is applied to both.
- The modifier class works right up until the component is placed somewhere new. Every additional slot adds a variant, and the component's contract grows a boolean per context it might ever appear in (What a Component Owes Its Caller).
- The knowledge ends up in the wrong place: the page has to know the internals of the card in order to tell it how to lay itself out, which is exactly the coupling components exist to remove (Drawing Component Boundaries).
- A design system component published to other teams cannot know the layouts it will be dropped into, so a viewport-based rule is a guess about somebody else's page (Design Systems).
- The JavaScript workaround — a
ResizeObserverthat measures and toggles a class — puts a layout read and a style write on the main thread for every instance, after the browser has already laid the page out once (Layout Thrashing).
What is actually happening
In the browser, not in the framework.
container-typeon an element makes it a query container. Descendant rules can then ask about *that element's* size with@container, and the answer is the container's used size after layout.- Querying a box's size while styling its contents is circular unless the contents cannot change that size. So
container-typeestablishes containment:inline-sizeapplies layout, style and size containment in the inline axis, andsizeapplies it in both axes. This is not a hint — it changes how the box is sized (CSS Containment). container-type: inline-sizemeans the element no longer sizes its inline dimension to its content. It takes its inline size from its own parent, and the block size still grows with content.container-type: sizeremoves content-based sizing in both axes, so anautoheight collapses to nothing.- The container is always an ancestor. An element cannot query itself, which is why the query container is the card and the rules inside it target the card's children.
container-namelabels a container so a nested query can address a specific ancestor rather than the nearest one. Without a name,@containermatches the nearest eligible ancestor container, which in a nested layout may not be the one you meant.- Container query units —
cqi,cqb,cqw,cqh,cqmin,cqmax— are percentages of the query container rather than the viewport, which is what makes a genuinely self-contained fluid component possible (Fluid Layout First).
What this makes the browser do
And which of it is avoidable.
- Establishing containment gives the engine a guarantee it can exploit: work inside a contained subtree cannot affect layout outside it, so an invalidation can be bounded rather than propagated to the root (Style Invalidation).
- Evaluating
@containerhappens as part of layout, not before it: the container is laid out, its size becomes known, matching rules apply, and the subtree is laid out again. Containment is what guarantees this terminates. - A container resize that crosses a query boundary costs style plus layout for the contained subtree, and paint for whatever moved. The saving relative to the
ResizeObserverversion is that none of it involves the main thread or a second frame. - Many containers on one page each carry their own evaluation, but each is bounded by its own containment. A thousand contained cards is closer to a thousand small layouts than to one enormous one (content-visibility).
- The rules inside non-matching
@containerblocks ship and parse like any other CSS. This does not reduce bytes.
The component does not know the viewport
The mismatch is easiest to see with one page and two slots. The viewport is 1400 CSS pixels wide, so any width media query gives one answer for the whole document. But the card in the main column has roughly 900 pixels and the card in the sidebar has roughly 280, and a rule that lays the card out as a row is right for one and produces an overflowing mess in the other.
A container query asks a different question, one the media query cannot express: not "how wide is the page" but "how wide is the box I was put in". The answer is different for each instance, which is exactly what a reusable component needs.
Containment is the price, and it is a real one
Querying a box while styling its contents is circular: the contents could change the size that the query is reading. CSS resolves this by requiring containment — container-type guarantees the subtree cannot influence the container's size in the queried axis, which makes the evaluation terminate.
That guarantee is also a constraint on your layout. The moment you write container-type: inline-size, the element stops sizing itself to its content in the inline axis. With size, it stops in both axes, and a box with automatic height collapses. This is the source of essentially every "why did my component disappear" report about this feature.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| Adding `container-type: inline-size` | yes | yes | maybe | no | It applies layout, style and size containment in the inline axis. The element stops sizing to its content inline and establishes an independent formatting context — a genuine geometry change, applied the moment the declaration lands. |
| Adding `container-type: size` | yes | yes | maybe | no | Containment in both axes, so the element no longer sizes to content at all. An automatic height collapses to zero. This is the "my component vanished" case. |
| A container resizing across a `@container` boundary | yes | yes | yes | maybe | The query re-evaluates during layout, the subtree restyles, and it is laid out again. Containment is what guarantees the second pass cannot change the container and start a third. |
| The `ResizeObserver` equivalent | yes | yes | yes | maybe | Same rendering work, plus a main-thread callback per observed element, plus a forced layout read, plus a one-frame lag in which the wrong variant is on screen (Layout Thrashing). |
| Reading a `cqi` unit instead of `vw` | yes | maybe | maybe | no | Resolving against the container rather than the viewport is the same arithmetic against a different reference. It becomes layout-relevant only because the container size is itself a layout output. |
caveat Whether paint is touched depends entirely on what the matched rules change. A query that only swaps grid-template-columns still repaints the subtree, because the boxes moved.
1/* The card declares itself a query container. Naming is optional but makes2 nested layouts readable — and prevents binding to the wrong ancestor. */3.card {4 container-type: inline-size;5 container-name: card;6}7 8/* Base rules: what a browser without support, or a container of unknown9 size, gets. This has to be a real design, not a placeholder. */10.card__body { display: grid; gap: 0.75rem; }11 12/* The card asks about the card, and styles the card's CHILDREN. */13@container card (inline-size >= 30rem) {14 .card__body { grid-template-columns: 12rem 1fr; align-items: start; }15 .card__media { aspect-ratio: 4 / 3; }16}17 18/* Units relative to the query container, not the viewport. */19@container card (inline-size >= 20rem) {20 .card__title { font-size: clamp(1rem, 4cqi, 1.5rem); }21}The rules inside target .card__body and .card__title, never .card — an element cannot query itself, so a rule that styles .card inside @container card is silently inert. Note also that 4cqi is four percent of the *card's* inline size, so the same declaration produces different type in the sidebar and the main column without a second rule.
Which mechanism, and when
The two query mechanisms are not competitors; they answer questions about different things. Getting the division right is most of the value, and the division is stable: the page and the user are viewport questions, the component and its slot are container questions.
The option most people skip is the first one. A great many "we need container queries" requirements turn out to be continuous, and a clamp() with no containment side effects is a smaller change than establishing a query container.
Should this change be driven by the viewport, by the component's own box, or by nothing at all?
when The change is continuous: a size, a gap, a column count.
cost Almost nothing, and it should be tried first. The cost is that arithmetic is harder to read than a breakpoint (Fluid Layout First).
when The change is about the page or the person: page arrangement, print, colour scheme, reduced motion, pointer type.
cost Every instance of a component gets the same answer regardless of where it was placed (Media Queries Beyond Width).
when The component appears in more than one slot and must fit whichever it was given (What a Component Owes Its Caller).
cost A container-type with real layout consequences, plus a subtree that can no longer size its container in the queried axis.
when The usual answer in a real system: media queries for page arrangement, container queries inside the components that page arranges.
cost Two mechanisms interacting. A container nested inside a media-queried region is genuinely hard to hold in your head, and harder to debug.
when You need the *value*, not a breakpoint — a canvas that must be redrawn at its exact pixel size, or a chart that recomputes ticks (Web Workers and the DOM Boundary).
cost Main-thread work per observation, a layout read you asked for, a one-frame lag, and a resize loop if the callback changes the observed box.
How to build it
Most important first.
- Try fluid first. If the change is continuous — a gap, a font size, a column width —
clamp()andauto-fitneed no container at all and no containment side effects (Fluid Layout First). - Put
container-type: inline-sizeon the component's outer wrapper, and write the queries against the component's own children. That keeps the whole contract inside the component. - Prefer
inline-sizeoversize. Block-axis containment is rarely what you want and is the direct cause of the "my component vanished" report. - Name your containers when layouts nest.
@container card (…)is readable and unambiguous; an unnamed query in a nested layout silently binds to the nearest container, which may be a grid cell you forgot about. - Use media queries for the page and the user — arrangement, print, colour scheme, motion, pointer — and container queries inside components. That division is the one that stays true as the product grows (Media Queries Beyond Width).
- Ship a sensible unqueried base. The rules outside
@containerare what a browser without support, and a component in a container of unknown size, will use (Polyfills vs Transpilation).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Container queries change appearance, never DOM order — so unlike a JavaScript re-render, they cannot silently reorder focus. That is a genuine accessibility advantage of doing this in CSS (Keyboard Operability).
- A compact variant that removes a label, a caption or a description removes it from the accessibility tree too. If the information is needed, keep it and make it visually secondary rather than deleting it (Semantics Before ARIA).
- Target sizes must survive the compact variant. A card that shrinks its action buttons in a narrow container is shrinking them for exactly the users most likely to be on a narrow container with a coarse pointer (Media Queries Beyond Width).
- Because containers respond to their own size, a component inside a zoomed page reflows without needing the page to cross a breakpoint. That is a reflow win — but test it, because a container sized in
vwinherits the viewport's problems anyway (The Viewport and Device Pixels). - A truncation-on-narrow rule (
text-overflow: ellipsis) hides text from sighted users but leaves the full string in the accessibility tree, so the screen-reader and visual experiences diverge. Decide which one is correct and make them agree.
What can go wrong
- A rule that never matches because the element tried to query itself.
@containerinside.cardasking about.cardis silently inert — no error, no warning, just a style that never applies. container-type: sizeon a box with automatic height. Size containment removes content-based sizing in the block axis, the box collapses to zero, and everything inside it disappears from view while remaining in the DOM.- An unnamed nested query binding to the wrong ancestor. The card is inside a grid cell that is also a container, and the query answers about the cell.
- Containment breaking a layout that depended on a child growing its parent — a
float-based ordisplay: contentsarrangement, or a shrink-to-fit wrapper. The containment did exactly what it promised and the layout relied on the opposite. - A query and its container fighting: rules inside
@containerthat change the container's own size. Containment prevents an infinite loop, but the result is a layout that is not what either rule intended. - The mitigation failing: falling back to a
ResizeObserverpolyfill that reintroduces exactly the main-thread cost and the one-frame lag the native feature removed.
- A container whose size depends on a late-arriving web font or image settles after those load, so a query can flip after the user has already read the first layout (Images and Fonts).
- Nested containers settle outward-in over successive layout passes: the outer container resolves, the inner one measures, and a query on the inner one may change on the second pass.
- A container query and a media query can cross their boundaries in the same resize, in an order the stylesheet does not control. Rules that assume one implies the other will disagree at the margin.
- Nothing here is enforced as a boundary. A compact variant that hides an action leaves the action in the DOM, focusable and callable (Authorization-Aware UI).
- Container queries respond to size, and size can be influenced by content. An element sized by user-supplied text can be pushed across a query boundary by that text — a layout state selected by a stranger, which matters if a query boundary hides or reveals something.
- Containment does not cross a shadow boundary in the way people assume: a query container in the light DOM does not automatically become the query container for a shadow tree's internals unless the tree is arranged for it (Shadow DOM and the Composed Tree).
- "Container queries replace media queries." They replace the *misuse* of media queries for component sizing. Page arrangement, print, colour scheme, motion and pointer are still viewport-and-user questions.
- "
container-typeis just a hint." It establishes containment and changes how the element is sized. Adding it to an existing layout can change that layout before you write a single query. - "I can query the element I am styling." The container is always an ancestor. This is the single most common first mistake.
- "Containment means my component is isolated." Style containment here is narrow; it is not a shadow root and it does not stop the cascade reaching in (Shadow DOM and the Composed Tree).
- "
cqiis likevw." It is a percentage of the query container's inline size, so its value depends on where the component was placed — which is the whole feature and also why a straycqiin a global stylesheet is confusing.
Measuring it, and what changes in the field
- In the elements panel, an element that is a query container is marked as such, and the computed styles show the containment that
container-typeapplied. If a rule is not applying, this is the first thing to check (A Mental Model of the Devtools). - The rules panel shows
@containerblocks alongside@mediaones, including which ones matched — the fastest way to find a query bound to the wrong ancestor. - Compare a container-query implementation against the
ResizeObserverversion in the Performance panel: the observer version shows script plus a forced layout per instance, the CSS version shows layout only (Debugging Rendering and Jank). - Watch for resize-loop errors in the console. They are the signature of the JavaScript approach and they do not occur with the CSS one.
- Field data is the only honest answer to "can we use this yet" — your own share of sessions in browsers that do not support it, not a general support table (Real User Monitoring).
- On a slow device the CSS version wins by more, because the JavaScript version's measure-and-write cycle is main-thread work that competes with everything else (Long Tasks).
- With hundreds of containers on one page — a long feed of cards — containment is what keeps the cost bounded. Without it, one card resizing would invalidate layout upward (List Virtualization).
- On first load, container queries apply during the first layout, so the compact variant is correct before hydration. The JavaScript version renders the wrong variant first and corrects it, which is a visible shift (Visual Stability).
- In an older in-app webview or a locked-down enterprise browser, support may be absent long after it is universal on the desktop. The unqueried base rules are what those users get, so they have to be a real design, not a placeholder.
- In a long-lived tab, the container query keeps working across every layout change without any listener to leak or forget (Memory Leaks).
- You are trading a page-level concept for a component-level one, and gaining a second query mechanism to hold in your head. A nested container inside a media-queried region is genuinely harder to reason about than either alone.
- Containment is a real constraint, not a free annotation. Some layouts — shrink-to-fit wrappers, content-driven parents — cannot become query containers without changing what they do.
- Component-owned responsiveness means the page can no longer override it by passing a modifier. That is the point, and it is occasionally exactly what you needed to do (Composition and Slots).
- Debugging moves from "which breakpoint am I in" to "which ancestor is my container and how wide is it", which requires reading the DOM rather than the window size.
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 core model —
container-typeestablishes containment,@containerqueries an ancestor, container units resolve against that ancestor — is specified in CSS Containment and behaves the same across engines that implement it. - SPEC-EVOLVINGThe feature is still growing around its edges: size queries landed first, style queries and
container-typekeyword additions have followed at different rates, and container units are specified alongside them. Treat any list of what is available as a snapshot and check the current specification rather than a tutorial. - BROWSER-SPECIFICSupport arrived in the major engines within a comparatively short window, but in-app webviews, embedded browsers and managed enterprise fleets lag desktop release notes by a long time — so the question is not "is it supported" but "what share of *your* sessions support it", answered from your own field data rather than a support table.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — this is dependency inversion applied to layout: the component depends on an abstract "available space" rather than on the concrete page that placed it, and the page stops needing to know the component's internals.