Container Queries in Components
A media query asks about the viewport. A component almost never wants to know about the viewport — it wants to know how much room it has been given. A card in a 300-pixel sidebar and the same card in a 900-pixel main column are the same component with the same markup and the same shadow tree, and until container queries shipped there was no declarative way for it to lay itself out differently in the two positions. The workarounds were all bad: a size attribute the consumer had to set correctly, a ResizeObserver writing classes from JavaScript, or a hard rule that the component only works at one width.
Container queries close that gap. An ancestor declares itself a query container with container-type, and descendants style themselves against that ancestor’s size with @container. For components the fit is exact, because :host can be the container: the element that receives the size is the element the design system owns. This is the parent topic for size-responsive component styling inside Styling, Theming & CSS Encapsulation.
container-type and container-name (and the container shorthand), the @container conditional group rule, and the container query length units cqw, cqh, cqi, cqb, cqmin, and cqmax. The containment behaviour that makes size queries possible — layout, style, and size containment — is defined in CSS Containment Module Level 2 and governs contain. Style queries (@container style(…)) are specified in the same Level 3 module and ship on a later timeline than size queries.
How Containment Makes the Query Possible
A size query has a circularity problem to solve. If a descendant’s styles can change the container’s size, and the container’s size selects those styles, the engine has no fixed point. CSS resolves this by requiring containment: declaring container-type: inline-size applies layout containment, style containment, and inline-size containment to that element. Inline-size containment means the element’s inline size is computed as if it had no contents, so it can be determined before its descendants are laid out. The engine then evaluates @container conditions against that already-known size and lays out the subtree once.
That requirement has a visible consequence that surprises people the first time: an element with container-type: inline-size no longer sizes itself to its contents in the inline direction. A display: inline-block card that used to shrink-wrap its text becomes as wide as its parent allows. The fix is not to remove the container declaration but to constrain the element explicitly — width: fit-content, a grid track, or a flex basis — because a query container must get its size from the outside.
container-type: size goes further and contains both axes, which means the element also stops sizing itself vertically to its contents. That is almost never what a component wants; it is for fixed-dimension regions like a dashboard tile. Use inline-size unless you have a specific reason not to.
The evaluation order in a frame is: resolve the container’s size from its own context, evaluate every @container rule whose container is that element, apply the matching declarations, lay out the contained subtree. Because the subtree cannot influence the container’s inline size, one pass suffices — which is exactly why container queries cannot produce the feedback loop discussed in ResizeObserver loop errors in components.
The Container Query API Surface
| Property or rule | Values | Notes |
|---|---|---|
container-type |
normal | inline-size | size |
normal allows style queries only. |
container-name |
none | one or more idents |
Optional; unnamed containers are still queryable. |
container |
<name> / <type> |
Shorthand, e.g. container: card / inline-size. |
@container <name>? (<condition>) |
size or style condition | Name is optional; without it the nearest eligible ancestor wins. |
| Size features | width, height, inline-size, block-size, aspect-ratio, orientation |
Range syntax works: (400px <= width < 800px). |
| Style features | style(--token: value) |
Queries a custom property’s computed value on the container. |
cqw / cqh |
1% of container width / height | Resolve against the query container, not the viewport. |
cqi / cqb |
1% of inline / block size | Writing-mode aware; prefer these. |
cqmin / cqmax |
smaller / larger of cqi and cqb |
Useful for square media. |
Two rules govern which container a query resolves against. Without a name, @container (width > 400px) binds to the nearest ancestor that is an eligible query container for the queried feature — so a size query skips over container-type: normal ancestors. With a name, it binds to the nearest ancestor carrying that name, and if none exists the rule simply never matches. Names are not global identifiers; several containers may share one.
Production Implementation Pattern
The card below makes its own host the container, so it responds to whatever space a consumer gives it without the consumer configuring anything.
class MediaCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>
/* The host is the query container. Consumers give it a size; it adapts. */
:host {
display: block;
container-type: inline-size;
container-name: card;
}
.layout { display: grid; gap: 0.75rem; }
.media { width: 100%; border-radius: 10px; }
.actions { display: flex; flex-direction: column; gap: 0.5rem; }
h3 { margin: 0; font-size: 1rem; line-height: 1.3; }
/* Wide enough for a two-column arrangement. */
@container card (inline-size >= 420px) {
.layout { grid-template-columns: 180px 1fr; align-items: start; }
.actions { flex-direction: row; }
h3 { font-size: 1.15rem; }
}
/* Wide enough to let the title breathe with fluid type. */
@container card (inline-size >= 640px) {
h3 { font-size: clamp(1.15rem, 3.2cqi, 1.6rem); }
.layout { grid-template-columns: 240px 1fr; gap: 1.25rem; }
}
</style>
<div class="layout">
<div><slot name="media"></slot></div>
<div>
<slot name="title"></slot>
<slot></slot>
<div class="actions"><slot name="action"></slot></div>
</div>
</div>`;
}
}
customElements.define('media-card', MediaCard);
<aside style="width: 300px"><media-card>…</media-card></aside>
<main style="width: 760px"><media-card>…</media-card></main>
Both cards are byte-identical. The sidebar copy stacks; the main-column copy goes two-column with fluid type. No attribute, no observer, no consumer knowledge of the component’s breakpoints — which is what makes the component genuinely portable across the layouts of applications the design system has never seen.
@container conditions are evaluated against an ancestor, never against the element that declares the container. Writing :host { container-type: inline-size } and then @container (width > 400px) { :host { padding: 2rem } } does nothing at all: the rule looks for a container above the host and finds either the wrong one or none. Every rule driven by the host's own size must target a descendant — which is why the pattern above wraps everything in .layout.
Common Failure Modes & Debugging Steps
1. The query never matches. Nine times out of ten the container declaration is on the wrong element or the queried name does not exist. Confirm in DevTools: Chromium’s Elements panel shows a container badge next to every query container, and hovering it highlights the queried box. A named query with no matching ancestor fails silently — there is no console warning for a typo in container-name.
2. The component stopped shrink-wrapping. container-type: inline-size applies inline-size containment, so the element takes its inline size from its parent instead of its contents. Add width: fit-content if the shrink-to-fit behaviour was intentional, or accept the block-level sizing and let the consumer’s layout decide.
3. Descendant content is clipped or overlapping. container-type: size applies size containment in both axes, so the element’s height no longer grows with its contents. Switch to inline-size unless you are deliberately building a fixed-dimension region.
4. Styles differ between the shadow tree and light-DOM children. A container query inside a shadow tree can only bind to containers the shadow tree can see — and that includes ancestors in the light DOM, because containment is a rendering concept that crosses shadow boundaries. Projected content, however, is styled by the consumer’s stylesheet, so a @container rule the component wrote does not apply to slotted nodes. This is the same reach limitation described in part and slotted selectors.
5. A named query silently stops matching after a refactor. Container names live in the same tree the rules do, so moving a @container card (…) rule from a shadow tree into a global stylesheet — or vice versa — changes which ancestors are visible to it. A rule in the light DOM cannot bind to a name declared on :host inside a shadow tree’s own stylesheet only if that host does not actually carry the property; but because :host styles apply to the host element itself, the container is visible from outside. The practical failure is the reverse: two components both using container-name: card nest inside each other, and the inner rule binds to the inner host while the author expected the outer one. Give names a component-specific prefix — wfc-card rather than card — the same discipline design tokens need, discussed in implementing design tokens with CSS custom properties.
6. Style queries appear not to work in Firefox. Size queries and style queries shipped on different timelines. @container style(--density: compact) requires Firefox 128, three years after size queries landed. Feature-detect with CSS.supports('container-type: normal') for the style-query path specifically, and treat style queries as an enhancement over an attribute-driven fallback until the floor moves.
Style Queries: Querying a Token Instead of a Size
The same at-rule accepts a style condition, which tests the computed value of a custom property on the query container. That turns a design token into a switch that descendants can respond to without inheriting anything themselves:
:host { container-type: normal; } /* style queries need no size containment */
/* A consumer sets the token; the component reorganises around it. */
@container style(--wfc-card-density: compact) {
.layout { gap: 0.35rem; }
.actions { display: none; }
}
@container style(--wfc-card-density: comfortable) {
.layout { gap: 1.25rem; }
}
<media-card style="--wfc-card-density: compact">…</media-card>
Two properties make this valuable in a design system. container-type: normal is the default, so every element is already a style-query container — no containment side effects, no sizing surprises. And because the condition tests a custom property, the component’s internal arrangement becomes a function of the same token vocabulary consumers already use for colour and spacing, rather than a separate attribute API. The tradeoff is the support floor, and that only a small subset of the condition syntax is available: equality against a declared value, not arbitrary comparisons.
Framework Interop
Container queries are pure CSS, so they need no adapter in any framework — a React, Vue, or Angular application that renders <media-card> gets the behaviour for free. The one integration concern is server rendering: because the query resolves at layout time in the browser, a server-rendered card has no computed layout until the client lays it out, so there is no flash-of-wrong-layout to avoid provided the component’s default (no-query) styles are the narrow case. Author mobile-first — the base rules describe the smallest arrangement, and each @container block adds to it — and the server-rendered HTML is correct before any query is evaluated.
Component libraries that ship a build step should be aware that some older CSS toolchains strip unknown at-rules. Verify that @container survives minification, particularly if the pipeline predates 2023.
Testing deserves a note of its own, because container queries change what a meaningful component test looks like. A unit test that renders the component at a single width proves almost nothing: the whole point is that the component has several arrangements. The useful test mounts the component inside a wrapper of a known width, asserts the computed style of an internal element, then changes the wrapper’s width and asserts the other arrangement. That works in any real browser runner. It does not work in a DOM emulation without a layout engine, because there is no layout and therefore no container size — which is a good argument for running component styling tests in a real engine, as visual regression testing shadow DOM sets out.
Performance & Memory Implications
Containment is a performance win, not a cost. Layout containment tells the engine that nothing inside the element can affect layout outside it, so an invalidation inside a card is confined to that card’s subtree instead of forcing a document-wide reflow. In a page with two hundred cards, that turns a global relayout into two hundred independent, small ones — and only the cards whose size actually changed are touched.
The cost to watch is a different one: @container rules multiply the number of style rules the engine must evaluate per element. This is ordinary selector-matching cost and it is small, but a component with fifteen breakpoints across four named containers is doing real work on every style recalculation. Consolidate to two or three meaningful arrangements, exactly as a well-designed media-query system would.
Compared with the JavaScript alternative, the win is decisive. A ResizeObserver writing classes costs a callback per resize, a style invalidation, and an extra layout pass, and it runs on the main thread inside the frame budget. A container query costs one style evaluation inside a layout the engine was already performing.
Browser Compatibility & Fallback Strategy
| Feature | Chromium | Firefox | Safari |
|---|---|---|---|
Size container queries (container-type, @container) |
105 | 110 | 16.0 |
Container query units (cqw, cqi, …) |
105 | 110 | 16.0 |
container-name and the container shorthand |
105 | 110 | 16.0 |
| Style queries for custom properties | 111 | 128 | 18.0 |
Those dates matter less than the shape of the fallback. Because @container is a conditional group rule, an engine that does not recognise it drops the whole block, and an engine that does not recognise container-type drops that declaration. Both failures are additive rather than destructive: the component keeps whatever the base rules said. That is why mobile-first authoring is not merely a convention here but the actual fallback mechanism.
Size queries have been interoperable since early 2023 and need no polyfill for a modern support matrix. Where an older engine must be supported, the graceful path is mobile-first authoring: the base rules produce a usable narrow layout, and an engine that does not understand @container simply ignores the enhancement blocks. Feature-detect in script only if a behavioural fallback is genuinely required, with CSS.supports('container-type: inline-size'), and treat the ResizeObserver route as the exception rather than the default.
Frequently Asked Questions
Can an element query its own size?
No. @container conditions are always evaluated against an ancestor query container, so a rule that targets the container itself never matches. Wrap the content in a child element and style that child instead — the standard shape for a component whose host is its own container.
Do container queries work across a shadow boundary?
Yes for containment: a shadow tree’s rules can bind to a query container in the light DOM, because containment is part of the rendering tree rather than the DOM tree. They do not, however, let a component style projected nodes — those are governed by the consumer’s stylesheet.
Why did my inline-block element become full width?
Because container-type: inline-size applies inline-size containment, which computes the element’s inline size as if it had no contents. Restore shrink-to-fit with width: fit-content if you need it, or let the parent layout supply the width.
Should I use cqi or cqw?
Prefer cqi. It resolves against the container’s inline size, so it behaves correctly in vertical writing modes and right-to-left contexts, whereas cqw is locked to the physical width.
Related
- Styling, Theming & CSS Encapsulation — the parent section for component styling.
- Making a Host a Query Container — the
:hostdeclaration and its sizing consequences. - Container Query Units Inside Shadow DOM — how
cqiresolves across the boundary. - Container Queries vs Media Queries for Components — choosing between them deliberately.
- CSS Scoping in Shadow DOM — how these rules participate in the cascade.