ResponsiveGENERALSPEC-EVOLVINGBROWSER-SPECIFIC

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 question

The same card is in a wide main column and a narrow sidebar. How does it know which one it is in?

The user intent

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 obvious build

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.

Why it breaks

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.

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

What is actually happening

In the browser, not in the framework.

  • container-type on 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-type establishes containment: inline-size applies layout, style and size containment in the inline axis, and size applies it in both axes. This is not a hint — it changes how the box is sized (CSS Containment).
  • container-type: inline-size means 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: size removes content-based sizing in both axes, so an auto height 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-name labels a container so a nested query can address a specific ancestor rather than the nearest one. Without a name, @container matches 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 @container happens 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 ResizeObserver version 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 @container blocks 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.

One viewport, two slots, two correct answers
one answer for the whole pagesame rulesame ruleright rule, wrong boxcard measures its own containerViewport: 1400 CSS px@media (min-width: 900px)Card in main column (~900px)Card in sidebar (~280px)Row layout — fitsRow layout — overflows@container (inline-size >= 30rem)Stacked layout — chosen by the card
UserLLMAgentToolDataDecisionHumanGuardrail

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.

What container queries actually cost
ChangestylelayoutpaintcompositeWhy
Adding `container-type: inline-size`yesyesmaybenoIt 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`yesyesmaybenoContainment 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` boundaryyesyesyesmaybeThe 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` equivalentyesyesyesmaybeSame 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`yesmaybemaybenoResolving 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.

A self-contained card
1/* The card declares itself a query container. Naming is optional but makes
2 nested layouts readableand 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 unknown
9 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.

Viewport, container, or neither

Should this change be driven by the viewport, by the component's own box, or by nothing at all?

Make it fluid — no query

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).

Media query

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).

Container query

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.

Both, at different levels

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.

`ResizeObserver` and a class

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() and auto-fit need no container at all and no containment side effects (Fluid Layout First).
  • Put container-type: inline-size on 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-size over size. 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 @container are 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 vw inherits 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

Failure modes
  • A rule that never matches because the element tried to query itself. @container inside .card asking about .card is silently inert — no error, no warning, just a style that never applies.
  • container-type: size on 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 or display: contents arrangement, 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 @container that 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 ResizeObserver polyfill that reintroduces exactly the main-thread cost and the one-frame lag the native feature removed.
What can arrive out of order
  • 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.
Security
  • 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).
Misreads
  • "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-type is 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).
  • "cqi is like vw." 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 stray cqi in a global stylesheet is confusing.

Measuring it, and what changes in the field

How you would see this
  • In the elements panel, an element that is a query container is marked as such, and the computed styles show the containment that container-type applied. If a rule is not applying, this is the first thing to check (A Mental Model of the Devtools).
  • The rules panel shows @container blocks alongside @media ones, including which ones matched — the fastest way to find a query bound to the wrong ancestor.
  • Compare a container-query implementation against the ResizeObserver version 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).
Slow device, slow network, large data, old tab
  • 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).
What this costs
  • 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-type establishes containment, @container queries 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-type keyword 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.

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