Stack Patterns — Episode 14
Every web developer has written this hack. A card component lives in a sidebar at 280 pixels on one page and on a dashboard at 1100 on another. You want the badges to disappear in the narrow case and the layout to switch from horizontal to vertical. You reach for JavaScript:
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
entry.target.classList.toggle('narrow', entry.contentRect.width < 400);
}
});
observer.observe(card);
You then write the CSS twice, once for the parent class and once again for the .narrow case. You hope the observer fires before the first paint, and most of the time it does, sometimes only after a flash of unstyled content. You move on, because the alternative is to wrap the component in a width-aware HOC, install a layout library, or accept the inevitable hydration mismatch in your SSR pipeline.
Container queries make all of that go away.
A Brief, Slightly Embarrassing History
The idea is older than most reading this article. The early 2010s saw a steady stream of element queries discussions in the front-end community, with polyfills (EQCSS and others) and a long mailing-list debate about what the missing tool should look like. The proposals all stumbled on the same architectural problem: querying an element's own size while inside the layout pass that produces that size invites circular layout, which is roughly the equivalent of asking a function for its own return value before it has finished returning.
The fix took the better part of a decade and required a quiet redefinition of the problem. The element under styling cannot query its own size; that path leads to circularity. But the element's containing context can. The work that became Container Queries, originally drafted in CSS Containment Module Level 3 and now living in CSS Conditional Rules Module Level 5, was championed by Miriam Suzanne (Invited Expert at the CSS Working Group) with contributions from many others. An element marks itself as a container and provides a stable size context to its descendants. The descendants then query that context. No circularity, no observer, no JavaScript.
The implementations followed quickly: Chrome 106 and Safari 16 in September 2022, Firefox 110 on 14 February 2023. Global usage is just over 94 per cent as of March 2026. The feature has been cross-browser stable for more than three years.
The Pattern
The simplest case takes two declarations:
.card {
container-type: inline-size;
}
@container (max-width: 400px) {
.card h3 { font-size: 1rem; }
.card .badge { display: none; }
}
container-type: inline-size tells the browser to watch the inline dimension (the writing-mode-aware horizontal axis in Latin scripts). The @container rule then matches when that dimension drops below 400 pixels. Rules inside the block apply to descendants of .card, never to .card itself.
There is also container-type: size, which watches both axes. Use it sparingly: size containment forces the container's block size to be intrinsic, which can collapse otherwise auto-height layouts in surprising ways. The inline-size form is the safe default for component-level responsiveness, and the form most CSS examples reach for.
When components nest, the closest matching ancestor wins. A query without a container-name matches the nearest ancestor with container-type set. To target a specific level, name your containers:
.sidebar { container-name: side; container-type: inline-size; }
.card { container-name: card; container-type: inline-size; }
@container card (max-width: 400px) {
/* only the card matters here, not the sidebar */
}
The Length Units That Travel With the Container
Container queries also bring four new length units that resolve against the container rather than the viewport:
-
cqw: 1% of the container's width -
cqh: 1% of the container's height -
cqi: 1% of the container's inline size (writing-mode-aware width) -
cqb: 1% of the container's block size (writing-mode-aware height) -
cqminandcqmaxare also defined, taking the smaller or larger ofcqiandcqb
Combined with clamp(), these allow typography that scales with the component, not the viewport:
h3 { font-size: clamp(1rem, 4cqi, 1.5rem); }
Drop the same component into a 280-pixel sidebar and a 1100-pixel main column, and its heading scales appropriately in both, without writing any breakpoints. The vocabulary that responsive design has wanted for fifteen years has finally arrived on the right axis.
Why It Works
The cleverness, as with @scope in the previous episode, is what container queries deliberately do not do. They do not query the element being styled; they query the size of an ancestor. The cycle that broke every previous attempt at element queries simply does not arise.
A second piece of cleverness: the container-type declaration triggers CSS containment for the chosen axes. The browser knows that nothing outside the container can affect what is inside it, and vice versa, for the purpose of size calculations. That makes the cost of container queries predictable: each container is a self-contained layout unit, evaluated once.
The performance question, which used to dominate discussions of element queries, has therefore become a much smaller question. Each container has a cost (a containment scope), but it is a known cost. You do not pay for queries you do not write.
Combined With :has()
Container queries become properly powerful in combination with :has(), the parent-aware selector covered in episode 5 of this series. A component now reacts to two independent axes at once: its own size, via @container, and its own content, via :has().
A common pattern: cards switch to a vertical layout when narrow, but only if they actually contain an image. A text-only card at the same width keeps its inline form.
.card {
container-type: inline-size;
display: grid;
grid-template-columns: auto 1fr;
}
@container (max-width: 400px) {
.card:has(img) {
grid-template-columns: 1fr;
}
}
The same approach scales: badges that disappear only when the container is narrow AND there are more than two of them; a sidebar widget that switches layout only when it contains a form; a section heading that changes weight when it is followed by a long article. None of these require JavaScript, a class toggle, or a render hook.
The composability is the broader point. Each modern CSS feature (container queries, :has(), @scope, @layer, view transitions) is independently useful, but combinations multiply their value. The platform has spent a decade quietly assembling a vocabulary in which most former JavaScript responsibilities for layout and conditional styling become CSS again.
Honest Limitations
Three things to know before you ship container queries to production.
First, container-type: size disables auto-height. If you want a container to react to changes in its own height (rare, but possible), the container must have a defined height rather than letting its content determine it. For most components, you only care about width, and inline-size is the correct choice. Reach for size only when you genuinely need both axes.
Second, style queries (the form @container style(--theme: dark) { ... }) are a separate, newer feature. As of 2026 they are supported in Chrome 111+ and Edge 111+ only; Firefox and Safari are still developing support. They allow a component to query the value of a custom property on its container, rather than its size, and are powerful for theming and design tokens. Until cross-browser parity arrives, treat them as progressive enhancement rather than a default tool.
Third, container queries do not propagate across iframe boundaries or shadow root boundaries. A widget embedded in an iframe queries its own document's containers, not the parent page's. This is generally what you want; it is worth knowing if you build embedded components that span those boundaries.
When to Use
Anywhere a component lives at more than one width. Cards in a sidebar and a main grid. Article previews in a related-articles strip and a featured slot. Dashboard widgets that the user can resize. Embedded widgets where you do not control the host layout. Anywhere your team currently maintains two or three classes (.card, .card--narrow, .card--wide) coordinated by JavaScript.
For a new design system, build it in containers from the start. Each component sets a container-type on its outer element and queries it from within. Page-level media queries become a layer above, handling viewport-scoped concerns: navigation collapse, hero sizing, things that genuinely depend on the device. Component-level concerns drop a level and stay there.
The Layout, Civilised
Episode 11 of this series gave native page transitions; episode 12 gave a built-in deep clone; episode 13 gave native CSS scoping. Each replaced a stack of build-time tooling and runtime libraries with a few lines of standard CSS or JavaScript. Container queries belong to the same lineage: a feature the platform has been quietly building for a decade, while the framework ecosystem invented and reinvented increasingly elaborate workarounds.
Three years on from cross-browser support, the workaround code is still in production codebases everywhere, and the platform feature still has under-used potential. If your team is still measuring component widths in JavaScript, the cascade has been waiting.
Read the full article on vivianvoss.net →
By Vivian Voss — System Architect & Software Developer. Follow me on LinkedIn for daily technical writing.

Top comments (0)