What you'll learn
Quick Answer
A container query applies styles based on the size of an ancestor element rather than the viewport. You mark an element as a query container with container-type: inline-size, then write @container (width > 400px) rules that target its descendants. The same component can then look different in a narrow sidebar and a wide main column with no change to its markup.
What media queries cannot do
A media query only ever measures the viewport, and that breaks component reuse. Say a product card switches to a two-column layout when there is room:
@media (min-width: 600px) {
.card { display: grid; grid-template-columns: 200px 1fr; }
}At the top of the page on a 1400px screen it looks right. Now drop the same card into a 320px sidebar on that same 1400px screen. The media query still sees 1400px, so the card renders two columns crammed into 320px. Its appearance depends on where the browser window is, not on how much room the card actually has.
Container queries invert this. The card measures its container and styles itself from that. One component definition then works in the sidebar, in a three-up grid, and in a full-width hero, with no per-context overrides and no JavaScript.
This is the exact situation design systems hit: a card, a media object, or a stat tile that has to look right everywhere it is dropped. Before container queries the workarounds were a resize observer in JavaScript, or a pile of context classes like .card--in-sidebar that every consumer had to remember to apply. Container queries move that decision into the component's own CSS, where it belongs.
Declaring a query container
Mark an ancestor as a container with container-type:
.card-wrap {
container-type: inline-size; /* size | inline-size | normal */
container-name: card; /* optional label */
}
/* shorthand for both */
.card-wrap { container: card / inline-size; }inline-size is what you want almost always: it lets you query the container's width and applies containment only on that axis. size enables width and height queries but needs the container to have an explicit height, or it collapses to zero. normal allows style queries only, no size queries.
You need a real wrapper element. The container and the elements you style with @container must be different elements, so the common structure is an outer .card-wrap holding an inner .card that the queries target.
Writing the queries
An @container block looks like a media query. Name the container to bind the rule to a specific one, or omit the name to match the nearest ancestor container:
@container card (width >= 400px) {
.card { display: grid; grid-template-columns: 120px 1fr; }
}
@container card (width < 400px) {
.card__meta { display: none; }
}The modern range syntax (width >= 400px) reads more directly than min-width, and both forms work. Combine conditions with and, or, and not:
@container card (400px <= width <= 700px) {
.card__title { font-size: 1.25rem; }
}Rules resolve against the nearest ancestor that is a container. If several ancestors qualify, the closest wins, which is exactly why container-name exists: it lets a deeply nested component query a specific outer container and skip the ones in between.
Container query units
Alongside the at-rule, container queries add length units measured against the query container instead of the viewport:
cqw- 1% of the container's widthcqh- 1% of the container's heightcqi- 1% of the container's inline sizecqb- 1% of the container's block sizecqmin/cqmax- the smaller / larger ofcqiandcqb
In a normal horizontal writing mode, cqi and cqw are the same value; they diverge in vertical writing modes. A typical use is fluid type that scales with the component rather than the page:
.card__title { font-size: clamp(1rem, 4cqi, 2rem); }If an element uses these units but has no ancestor container, they fall back to the small viewport units for that axis, so the value still resolves to something sensible.
The gotchas
You cannot query the element you made a container. @container rules only affect descendants. If you write .card { container-type: inline-size } and then expect @container to restyle .card itself, nothing happens. Always nest: container on the wrapper, styles on a child.
Containment changes layout. Setting container-type to size or inline-size applies size containment, meaning the container's size in the contained axis can no longer be derived from its contents. A container that previously grew to fit its children may now collapse, so give it a width from its own layout context: a grid track, a flex basis, a percentage.
Height queries need an explicit height. container-type: size with height: auto gives you a zero-height container and queries that never match. Use inline-size unless you genuinely need to react to height.
Style queries
Container queries can also test the computed style of a container, not just its size:
.panel { container-name: panel; }
@container panel style(--mode: dark) {
.card { background: #111; color: #eee; }
}This is useful for passing a mode or variant down without adding class names at every level. Set --mode: dark on the panel and every descendant card reacts.
In shipping browsers today, style queries reliably work with CSS custom properties only. The specification also allows querying regular properties such as style(background-color: red) and range comparisons like style(--count > 4), but support for those is still limited, so treat custom-property queries as the safe subset. Style queries do not apply size containment, so they work on a plain container-name with container-type: normal.
The pattern this replaces is the descendant selector: .panel-dark .card { ... } repeated for every element inside the panel. With a style query you set one custom property on the panel and each component decides for itself how to respond, which keeps the theming logic next to the component instead of scattered across a parent stylesheet.
