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, rather inevitably, 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 higher-order component, install a
layout library, or accept the inevitable hydration mismatch
in your server-side rendering pipeline.
Container queries make all of that go away. Rather quietly.
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 like EQCSS 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 browser engineers, sensibly, declined to offer one.
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.
Implementations followed quickly. Chrome 106 and Safari 16 shipped in September 2022; Firefox 110 followed on 14 February 2023. According to caniuse, 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 per cent of the container's widthcqh: 1 per cent of the container's heightcqi: 1 per cent of the container's inline size (writing-mode-aware width)cqb: 1 per cent 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. One does not pay for queries one does 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 one ships container queries to production.
First, container-type: size disables auto-height.
If one wants 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, one only cares about width, and
inline-size is the correct choice. Reach for
size only when one genuinely needs 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 one wants; it is worth knowing if one builds 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 one does 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.
Container queries: cross-browser stable since Firefox 110 on 14 February 2023, global usage 94 per cent in March 2026. Two declarations:
container-typeon the parent,@containerrules on the descendants. No JavaScript, no observer, no double class.cqi,cqb,cqw,cqhfor typography that travels with the component.:has()for content-aware variants on the same axis. The cascade was capable all along.