Container queries: size a component by its parent, not the viewport
You build a card component. In the sidebar it should stack its image above its copy; in the main column it should put them side by side. With media queries you write the breakpoint against the viewport — but the viewport did not change. The card moved into a narrower parent, and nothing about the screen told you that. Container queries fix the mismatch: instead of asking how wide the browser window is, they ask how wide the container is. That is the question a reusable component actually needs answered before it can lay itself out.
The container is the unit of reuse
A media query is the right tool when the page is the thing that changes — a phone, a tablet, a desktop. But the moment you build a component and drop it into several parents, the viewport is the wrong ruler. A card in a sidebar and the same card in a hero are the same component under different parents, and the layout decision belongs to the parent’s width, not the window’s. Container queries make the component self-describing: it declares the widths it needs and reflows itself wherever it lands. That is the difference between a component that is reusable and one that is merely copy-pasteable.
Declaring a query container
Any element can become a query container by giving it a container-type. The
inline-size value queries the horizontal axis only, which is what most layouts
care about and what avoids the expensive full-size variant. A container-name
lets you target a specific ancestor when several are query containers:
.product-card {
container-type: inline-size; /* this element is now a query container */
container-name: card; /* name it so queries are unambiguous */
}
@container card (min-width: 480px) {
.product-card__media {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
The @container rule reads almost like a media query, but the condition is the
container’s width, not the viewport’s. The card now carries its own responsive
behaviour with it, and the sidebar and the hero both get the right layout with
zero page-level breakpoints.
The JavaScript you no longer need
Before container queries, the same effect meant a ResizeObserver and a
class toggle — JavaScript doing what CSS can now do declaratively:
const card = document.querySelector('.product-card');
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const wide = entry.contentBoxSize[0].inlineSize >= 480;
card.classList.toggle('is-wide', wide);
}
});
observer.observe(card);
That works, but it is imperative, per-component, and easy to forget on the
eleventh card you add. Container queries move the decision into the stylesheet
where it belongs, and the browser handles the observation for you. ResizeObserver
still earns its keep for things CSS cannot express — reacting to a height, or
running arbitrary logic on resize — but for “reflow when my parent gets wider”,
the @container rule is the simpler, more robust answer.
Where the trade-off bites
Container queries are not a free upgrade. A query container creates a new
containment context, so position: fixed children and some sizing behaviour
resolve against the container rather than the viewport — the same surprise
contain and transform already teach you. And because the browser must track
each container’s size, a page with hundreds of query containers pays a real
cost; reach for them on the components that genuinely move between parents, not
on every element. The rule of thumb: if the component’s layout depends on its
own parent’s width, use a container query; if it depends on the device, keep the
media query. Most reusable components are the former, which is why this is the
tool that finally makes “responsive component” mean something.