Article · Personal blog
Container queries: when a component responds to its slot, not the screen
The shift from media queries to @container, inline-size containment, the cqi unit and the gotchas a card component runs into in a sidebar.
For years a card that lived in a wide main column and the same card squeezed into a narrow sidebar needed two different sets of styles, glued together by a media query that knew nothing about either slot. Container queries finally let the component ask the only question that matters: how much room do I actually have here?
From the viewport to the slot
A media query answers a global question — how wide is the screen — and every component on the page hears the same answer. But a card does not care about the screen. It cares about the column it was dropped into. A 1440px desktop can host a 240px sidebar and a 900px main area at the same time, and the same component renders in both.
With media queries you encode the slot indirectly: if the screen is wide, the sidebar exists, so the card in it is narrow. That coupling breaks the moment someone reuses the card somewhere the layout author never imagined.
A media query asks how big the page is. A container query asks how big the component’s slot is. Only the second question is the component’s business.
container-type and why containment is required
To query an element you first declare it a container. The common case is inline-size: the browser tracks the container’s inline dimension (width in horizontal writing modes) and exposes it to descendant @container rules.
.card-slot {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 30rem) {
.card { grid-template-columns: 12rem 1fr; } /* wide slot: media beside text */
}
Why must containment be involved at all? Because a layout where the container sized itself to its children, while its children sized themselves to the container, is circular. container-type: inline-size applies size containment on the inline axis: the element’s inline size is computed independently of its contents, which breaks the loop. That is also the constraint to remember — a contained element no longer grows to fit its children on that axis.
The cqi unit and friends
Container queries bring container-relative length units, and they are the part people underuse. 1cqi is 1% of the query container’s inline size; 1cqw is 1% of its width. There are cqb and cqh for the block axis, plus cqmin and cqmax.
This lets you scale type and spacing to the slot instead of the screen — fluid typography that finally tracks the component, not the window:
.card__title { font-size: clamp(1rem, 0.8rem + 2.5cqi, 1.5rem); line-height: 1.2; }
In a 240px sidebar that clamp resolves small; in a 900px column the same declaration grows the title — with no breakpoint and no JavaScript measuring anything.
You cannot query the element you contained
This is the gotcha that costs everyone an afternoon. A @container rule matches against the nearest ancestor container, not the element carrying container-type. So you cannot put container-type on .card and then write @container rules that style .card itself based on its own size.
The fix is structural: the container is a wrapper, the thing you restyle is a child.
- Put
container-typeon the slot or wrapper, never on the component root you want to adapt. - Give nested containers a
container-nameso an inner@containerdoes not accidentally resolve against the wrong ancestor. - Remember the cascade is unchanged —
@containeronly gates which rules apply; specificity still decides the winner.
In the file manager I worked on, the preview card sat in three different slots — a sidebar, a modal, a grid cell — with identical markup. The wrapper owned the container; the card just reacted. One component, three contexts, zero conditional rendering.
What it costs and where it bites
Size containment is not free conceptually. Once an element is a size container on an axis, its size on that axis no longer depends on its content there — which is exactly what you want for queries and exactly what surprises you when an inline-size container collapses because nothing gave it a width. Always make sure the container gets its inline size from the layout (a grid track, a flex basis, an explicit width).
A few things worth internalizing before you sprinkle @container everywhere:
- A query container only sees its own descendants; siblings and ancestors are invisible to it.
- Naming is cheap insurance — unnamed containers resolve to the nearest ancestor of the right type, which is rarely what you meant under nesting.
cqiunits and@containerrules both need an established container above them; with none, units fall back to the small viewport and rules simply never match.
The payoff is the part that changes how you build: components stop carrying assumptions about the page. A card I ship is responsive to wherever it lands, the layout owner decides the slots, and the two concerns stop leaking into each other. That separation is worth far more than the handful of breakpoints it deletes.