A Practical Guide to Container Queries: Size Queries, Style Queries, and Container Units
Use container queries in production with a media-query fallback pattern, side-by-side code showing when each approach is the right call for the component.
Container queries are the component-level responsive tool that finally matches how CSS Grid and flexbox actually build pages: a card in a sidebar, a widget in a dashboard cell, a callout in a narrow column. The production fallback pattern is direct. Write the container query. Guard it with @supports (container-type: inline-size). Leave a media-query approximation as the floor. Size queries change layout based on a container’s inline-size, not the viewport. Style queries read a custom property’s computed value. Container units (cqw, cqh, cqi) size children relative to the query container. The whole system shipped in Chromium and Safari by early 2024, with Firefox following later. Baseline marks size queries widely available since February 2024. The fallback is not a polyfill you write yourself. It is a media query that approximates the container width, or a ResizeObserver path if the layout truly depends on a parent’s dynamic size. The sections below show both versions side by side, say which one each case actually calls for, and tell you exactly when to use which.
Size Container Queries: Production Pattern
Start with the card component that switches from a horizontal row to a stacked column when its container narrows. The media-query version assumes the viewport width predicts the component’s available space. That breaks the moment the card moves from a wide page to a narrow sidebar. The container-query version measures the actual parent.
/* Media-query version: viewport-based approximation */
.card { display: flex; flex-direction: row; }
@media (max-width: 480px) {
.card { flex-direction: column; }
}
/* Container-query version: parent-based truth */
.card-list {
container-type: inline-size;
container-name: card-list;
}
@container card-list (max-width: 480px) {
.card { flex-direction: column; }
}
The second version is what the case calls for. The card’s behaviour depends on how much room its parent gives it, not on the viewport. The fallback pattern for browsers without container query support: keep the media query as the floor, and layer the container query inside a @supports guard so modern browsers get the precise behaviour.
/* Production fallback: @supports guard + media floor */
.card { display: flex; flex-direction: row; }
@media (max-width: 480px) {
.card { flex-direction: column; }
}
@supports (container-type: inline-size) {
.card-list {
container-type: inline-size;
container-name: card-list;
}
@container card-list (max-width: 480px) {
.card { flex-direction: column; }
}
}
The most common mistake: forgetting to declare container-type on the container element. @container matches no container and the query silently fails. The second: using container query length units (cqw, cqh) inside the query condition itself. That creates a circular dependency and invalidates the query. The condition measures the container. The units size children relative to it.
Query Syntax And Supported Features
A size query’s condition syntax follows the grammar: <container-query> = not <query-in-parens> | <query-in-parens> [ [ and <query-in-parens> ] | [ or <query-in-parens> ] ], where <query-in-parens> is either a size feature like (width: 30rem) or (orientation: portrait) or a style query. The supported size features are width, height, inline-size, block-size, aspect-ratio, and orientation. Use inline-size for most cases. It respects the writing mode and avoids surprises with horizontal scrolling.
Naming And The Container Shorthand
Container names are optional. The container-name declaration accepts none or a space-separated list of <custom-ident>. An unnamed container matches a @container with no name specified. A named container only matches queries that name it. The shorthand container: card-list / inline-size sets both name and type in one declaration. Name your containers when you have nested containers. The nearest ancestor with a matching name wins. A query without a name matches any ancestor that has a declared container-type.
What This Replaces
The media-query-only responsive grid that breaks when a component moves from a wide page to a narrow sidebar. That grid forced you to guess the component’s width from the viewport. The guess failed every time the component’s context changed: a sidebar, a modal, a split pane. Container queries remove the guess. The component measures its actual parent and responds to that.
Style Queries: Custom Property Condition
Style queries respond to the computed value of a custom property on the container, not to its size. The syntax is @container style(<property>: <value>) { <rule-list> }. This is component-variant logic in pure CSS: a theme flag, a priority level, a state that you flip by changing one custom property.
/* Media-query version: no equivalent, you would use a class toggle */
.card--alert { border-color: red; background: #ffe5e5; }
.card--success { border-color: green; background: #e5ffe5; }
/* Style-query version: read a custom property */
.card-container {
--state: normal;
}
@container style(--state: alert) {
.card { border-color: red; background: #ffe5e5; }
}
@container style(--state: success) {
.card { border-color: green; background: #e5ffe5; }
}
The style-query version is what the case calls for. Change –state on the container, and the component re-styles itself without JavaScript class toggling and without knowing which descendant needs the change. The style query reads the computed value of the custom property from the nearest ancestor container that has a container-type declared. container-type: normal is enough for style queries. They do not need a size container.
Engine support differs. Chromium shipped style queries in version 111. Safari followed in version 17. Firefox had not shipped them as of September 2026. Baseline marks style queries as limited availability. They are not yet safe for unrestricted production use. The fallback for style queries is not a media query. Media queries cannot read custom properties. The fallback is a class-based variant, or a ResizeObserver-driven custom property flip if you need the state to derive from a measurement.
The Constraint-Solving System
Custom properties plus calc() plus @container style queries form a constraint-solving system that behaves like a limited logic language. Chain conditions with and and or inside the style query parentheses. Combine style conditions with size conditions in the same @container rule. Treating this as dumb is the mistake. You can encode a surprising amount of component state logic without JavaScript. The flip side: when you need to react to JavaScript state (a fetch response, a user interaction), you still flip a custom property from the script. CSS cannot observe JavaScript state directly.
The failure mode for style queries: the custom property is set on a parent that does not match the expected inheritance chain, or the var() fallback syntax has a typo that silently fails. Check that the custom property is actually inherited to the container element, not to a sibling. Check that you are testing the computed value, not the specified value. A style query tests the computed value of the custom property on the container. @container style(–state: alert) matches when the container’s computed –state is alert, even if the specified value came from a different declaration.
Container Query Units: cqw, cqh, cqi
Container query length units size children relative to the query container. The six units: cqw (1% of query container width), cqh (1% of query container height), cqi (1% of query container inline size), cqb (1% of query container block size), cqmin (the smaller of cqi or cqb), and cqmax (the larger). The inline size is the width in horizontal writing modes. Block size is the height. Use cqi for width-based sizing in most layouts.
/* Media-query version: viewport units, viewport-relative */
.card {
font-size: clamp(1rem, 2vw, 1.5rem);
padding: 2vw;
}
/* Container-query unit version: parent-relative sizing */
.card-list {
container-type: inline-size;
}
.card {
font-size: clamp(1rem, 4cqi, 1.5rem);
padding: 4cqi;
}
The container-query unit version is what the case calls for when the card’s typography and spacing should scale with its parent, not with the viewport. A card in a narrow sidebar should type at a smaller size than the same card in a wide main column, even on the same viewport. Container units give you that with one declaration.
The Safari interop bug: older Safari releases that shipped container queries had bugs in container unit resolution. cqw and cqh would resolve to 0px or behave as their small-viewport equivalents in some compositions, particularly when the query container was not an ancestor of the element using the unit. The W3C CSS Containment Module Level 3 specification defines the units as resolving against the nearest query container. Safari’s early implementation had edge cases with nested containers and with containers that had no explicit size. If you see container units collapsing to zero in Safari, check the container’s own sizing. A container with height: auto and no content height can give cqh a zero basis.
The fallback for container units: viewport units (vw, vh) or a clamp() with rem-based floor and ceiling. The media-query approximation is less precise but safe. If you need true container-relative sizing in unsupported browsers, the ResizeObserver path is the only option. Observe the container’s content box, set a custom property with the pixel width, and use calc() with that custom property in place of cqi.
The CLS Connection
Container units can cause cumulative layout shift if you size an element before its container has laid out. The containment that container-type provides, specifically contain: layout style implied by container-type: size, helps reserve space. The contain-intrinsic-size property is the tool for elements that have no intrinsic height. Declare contain-intrinsic-size: auto 300px on a container that may be empty on first paint. The browser reserves the space and the page does not jump when the content arrives. Any CSS that reserves space before content loads (aspect-ratio, explicit dimensions, contain-intrinsic-size) reduces CLS contribution. The cost of not doing it is a visible shift that degrades user experience metrics.
Container Queries Media Queries Side by Side
The difference is the reference frame. Media queries respond to the viewport or device: the browser window, the screen, the print media. Container queries respond to the size of a container element: the parent that holds the component. The two are not interchangeable. They answer different questions.
| Question | Correct tool | Why |
|---|---|---|
| “Is the viewport narrow enough to switch to a single column?” | Media query | The viewport is the reference frame for page-level layout. |
| “Is this card’s parent narrow enough to stack the image above the text?” | Container query | The parent is the reference frame for component-level layout. |
| “Should the sidebar collapse to icons on small screens?” | Media query | The sidebar’s behaviour depends on the viewport, not on its own width. |
| “Should this widget’s typography scale with its panel?” | Container units | The panel is the reference frame for the widget’s internal proportions. |
Use media queries for the page skeleton: header, footer, main column count, navigation breakpoints. Use container queries for anything that could live in more than one place on the page. A component that appears in a sidebar and in a main column needs container queries. A page shell that only ever spans the viewport does not.
The grid-versus-flexbox distinction has the same shape: grid is two-dimensional with explicit row and column tracks; flexbox is one-dimensional and distributes along a single axis. Container queries do not replace media queries. They replace the misuse of media queries for component behaviour. The same way subgrid makes grid the correct choice for many component-internal alignments, container queries make component-level responsive behaviour correct. The distinction is not macro versus micro. It is which reference frame the condition measures.
The @supports Guard
The accepted guard is @supports (container-type: inline-size). This tests the container-type declaration’s parsing, which is the gate for size queries. Style queries need a separate guard. @supports (container-type: style) is not the right test. You test the style query syntax itself, which has no property value pair to probe. The practical guard for style queries is @supports selector(:has()). Not related, but both shipped in the same era and both signal a modern engine. The honest answer: style queries have no clean @supports test because they are an at-rule syntax, not a property. The @supports rule can test property:value pairs and selector() but not every at-rule. For style queries, the fallback is the class-based variant. Browsers without style queries get the class version.
The @supports guard is not a performance feature. It is a correctness feature. It prevents old browsers from parsing and discarding the container query rules, which would leave the component unstyled. The fallback media query or class rule sits outside the guard. Every browser gets a usable version.
@supports Container Queries Fallback Pattern
The fallback pattern is a three-layer stack. Layer one: the base styles, identical in every browser. Layer two: the media-query approximation that works everywhere. Layer three: the container query, guarded by @supports, which overrides the approximation in browsers that support it.
/* Layer 1: base */
.card {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Layer 2: media-query approximation (all browsers) */
@media (min-width: 50rem) {
.card { flex-direction: row; }
}
/* Layer 3: container query (progressive enhancement) */
@supports (container-type: inline-size) {
.card-list {
container-type: inline-size;
}
@container (min-width: 30rem) {
.card { flex-direction: row; }
}
}
The approximation is not perfect. The viewport breakpoint does not match the container breakpoint in every layout. It is close enough for the common case, and the container query corrects it where supported. The alternative fallback is the ResizeObserver polyfill path: observe the container’s content box, write the pixel width to a custom property, and use that property in a media-query-like condition via calc(). This path gives you true container-query behaviour in any browser. It costs JavaScript and a layout observation loop. Use it only when the component’s layout genuinely depends on the parent’s size in a way the viewport approximation cannot capture. A data visualisation that needs a minimum width to render legibly is the canonical case.
The performance-conscious question: what does a container query cost? The containment that container-type: inline-size implies (layout containment on the inline axis) has a cost. The browser cannot always skip layout work it would skip without containment. In practice, the cost is small for most components. It compounds when you have thousands of containers. Measure with the performance profiler, not with intuition. The ResizeObserver fallback is always more expensive than the native feature because it runs JavaScript on every resize event. Animating container query conditions is not supported. You cannot transition a container from one query state to another. The change is discrete. If you need a smooth transition, animate transform and opacity inside the query state, not the layout properties.
Complete Runnable Sample: Card Component
Here is the whole card component, with the media-query fallback and the container-query enhancement. Copy this into a test file and resize the parent to see the difference.
<div class="card-list">
<article class="card">
<img src="product.jpg" alt="" class="card__media">
<div class="card__body">
<h3>Product Name</h3>
<p>Description text that wraps naturally.</p>
<button>Add to cart</button>
</div>
</article>
</div>
.card-list {
container-type: inline-size;
container-name: card-list;
}
.card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.card__media {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
@media (min-width: 40rem) {
.card { flex-direction: row; }
.card__media { width: 40%; }
}
@supports (container-type: inline-size) {
@container card-list (min-width: 40rem) {
.card { flex-direction: row; }
.card__media { width: 40%; }
}
}
The case calls for the container query. The card-list could sit in a sidebar (narrow) or a main column (wide) on the same viewport. The media query gives a reasonable default for browsers that need the fallback. The container query refines it where supported. The aspect-ratio on the media image reserves space before the image loads, which reduces cumulative layout shift. The browser knows the height from the width and the ratio.
The common mistake in this sample: forgetting the container-name on the @container rule. If you name the container in the declaration, you must name it in the query. An unnamed @container matches any ancestor with a container-type, which could be a parent you did not intend. Name your containers when you have more than one.
Complete Runnable Sample: Style Query Variant
This sample shows a notification component that changes variant based on a custom property on its container. The fallback is a class toggle. The enhancement is the style query.
<div class="notification-shell" style="--tone: alert">
<div class="notification">
<p>Your session expires soon.</p>
</div>
</div>
.notification-shell {
container-type: inline-size;
}
.notification {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 0.5rem;
}
/* Fallback: class-based variant */
.notification-shell[style*="--tone: alert"] .notification {
border-color: #d33;
background: #ffe5e5;
}
/* Enhancement: style query */
@supports (container-type: inline-size) {
@container style(--tone: alert) {
.notification {
border-color: #d33;
background: #ffe5e5;
}
}
}
The style query is what the case calls for when the variant is set by a parent component that does not know the notification’s internal structure. The parent flips –tone, and the notification re-styles itself. The fallback uses the attribute selector on the style attribute. A hack, but it works everywhere. In practice, the class-based fallback is cleaner: add a modifier class alongside the custom property, and the class rule takes precedence in older browsers.
Style queries do not require a size container. container-type: normal on the shell is enough. The style query tests the computed value of –tone on the nearest ancestor that has a container-type declared. If no ancestor has one, the query matches nothing. The failure mode: you declare the custom property on the shell, but the shell has no container-type. The style query has no container to measure. Add container-type: normal to any element that should act as a style query container.
Style Queries In The Design System
The design-system author uses style queries for theme variants without duplicating the component’s markup. A button that needs a primary and ghost variant responds to –button-variant on its parent. The parent decides which variant to use based on its own state. The alternative, a class on every button, couples the parent’s state to the child’s class list. Style queries decouple them. The parent sets a property, and any descendant that knows the property responds. This is the same decoupling that custom properties gave you for theming, extended to conditional logic.
Complete Runnable Sample: Container Unit Sizing
This sample sizes a widget’s typography and spacing relative to its container, with a viewport-unit fallback.
<div class="panel">
<div class="widget">
<h2>Live Data</h2>
<p>Chart and summary.</p>
</div>
</div>
.panel {
container-type: inline-size;
container-name: panel;
}
.widget {
font-size: clamp(0.875rem, 3cqi, 1.25rem);
padding: clamp(0.5rem, 2cqi, 1.5rem);
}
/* Fallback: viewport units */
@supports not (container-type: inline-size) {
.widget {
font-size: clamp(0.875rem, 2vw, 1.25rem);
padding: clamp(0.5rem, 1.5vw, 1.5rem);
}
}
The container unit version is what the case calls for. The widget’s type and spacing should scale with the panel, not with the viewport. The clamp() provides a floor and ceiling so extreme container sizes do not produce unreadable text. The fallback uses viewport units, which is the closest approximation without container support.
The Safari interop bug: in Safari releases that shipped container queries before the specification’s resolution rules were fully implemented, cqi could resolve incorrectly when the panel had a percentage height inside a grid track. The symptom was text smaller than expected, or padding collapsing to the clamp floor. The workaround: use cqi (inline size) rather than cqh (block size). Inline size is more predictable in most layouts. If you absolutely need block-size-relative units, test in Safari 17.4 or later, and provide a fallback for older Safari.
Container query length units resolve to 0px in browsers without container query support if you use them outside a @supports guard. The @supports not guard above prevents that. Unsupported browsers get the viewport-unit version. Supported browsers get the container-unit version. If you forget the guard, the container units parse as invalid and the declaration is dropped, leaving the widget at the browser’s default font size. A silent failure that is hard to debug.
The Performance Cost: Layout, Paint, and Composite
Container queries do not add paint or composite cost by themselves. They change which declarations apply, and those declarations have their own costs. The question is what the declarations do. A container query that switches flex-direction from row to column changes layout. A container query that changes background-color only affects paint. The container query is a condition, not a property. It does not cost more than the properties it toggles.
The cost that container queries do add is containment. container-type: inline-size implies contain: layout style on the inline axis. This tells the browser that the element’s layout and style do not affect anything outside it. A benefit: the browser can skip work it would otherwise do to propagate changes. It has an edge case. If the container holds an element that escapes its bounds (a fixed-position child, a negative margin that extends outside), the containment clips or changes the behaviour. The containment is the mechanism that makes the container query efficient. It is not a free lunch.
The cumulative layout shift connection is more direct. When a container query changes layout, the change itself can cause shift if it happens after the user has started interacting with the page. The fix is the same as any responsive layout. Ensure that initial paint uses the correct state. Ensure that changes happen as the container resizes, not as a delayed reaction. Container queries react synchronously to container resize. They do not wait for a JavaScript observer. They reduce CLS compared to a ResizeObserver fallback that runs one frame late.
The animatable properties inside a container query are the same as anywhere else. Transform and opacity are the only compositor-friendly properties. Animating width, height, or font-size inside a container query triggers layout or paint on every frame. The compositor advantage is lost. If you need a smooth transition when the container query state changes, animate transform and opacity only. Or use a compromise: the container query switches a transform, and the transform animates.
The Real-World Interop Gap
Baseline status groups features by availability: widely available, newly available, or limited. Size container queries are widely available since February 14, 2024. All major engines support them. Style queries are limited availability as of September 16, 2026, because Firefox has not shipped them. Container units have the Safari interop bug noted above. “Shipped” is not the same as “correct.” Users on older device-locked browsers (iOS Safari on unsupported devices, Android WebView in apps that do not update) will get the fallback. That is the real gap: not the spec, not the engine, but the devices that cannot update. The fallback pattern exists for them. Check caniuse for current support data rather than trusting a snapshot percentage. The tail of old devices is always longer than the headline number suggests. Design for the tail.
What You Should Do Next
The single most practical thing to do next: audit your current responsive components. Mark each one as either viewport-dependent or parent-dependent. The viewport-dependent ones stay on media queries. The parent-dependent ones (cards, widgets, callouts, anything that appears in more than one context) get the three-layer fallback pattern: base styles, media-query approximation, and @supports-guarded container query. Start with one component, not all of them. Write the container query version. Keep the media query as the floor. Test in Safari with the interop bug in mind: use cqi, not cqh, and verify no zero-size resolution. The fallback pattern is not a compromise. It is the production pattern. Ship it.
Frequently Asked Questions
Do I need to use a ResizeObserver polyfill?
No, unless the component’s layout genuinely depends on the parent’s size in a way the viewport approximation cannot capture. The media-query fallback covers the common case. The ResizeObserver path is for data visualisations, complex grids that must not break, and components where a wrong breakpoint causes unusable layout.
Can I use container units (cqw, cqh) outside a container query?
Yes. Container query length units work in any declaration on any descendant of a container. They do not need to be inside a @container rule. The container is the nearest ancestor with a container-type declared. If no container exists, the units resolve to 0px.
Why is my container query not responding?
The most common cause is a missing container-type on the container element. The second is a name mismatch: the query names a container that does not exist, or the container has a name but the query is unnamed. The third is a circular dependency: using container units inside the query condition.