Stop Writing Media Queries for Components: A Practical Case for CSS Container Queries
Media queries answer "how wide is the viewport?" — but reusable components need to know "how wide is my space?" A practical guide to CSS container queries, with a working card example and the containment gotcha that collapses elements.
12 Jan 2026, 02:51 UTC

If you've ever built a card component that looks great in the main content column and then falls apart the moment someone drops it into a sidebar, you've hit the fundamental mismatch in responsive design: media queries answer the question "how wide is the viewport?" when what your component actually needs to know is "how wide is my space?"
Container queries close that gap. They let a component adapt to the size of its containing element, so the same card renders horizontally in a wide grid cell and stacks vertically in a narrow rail — no JavaScript, no viewport hacks, no duplicate "sidebar variant" classes. This post walks through how the feature works, a complete card example, and the one containment gotcha that trips up almost everyone the first time.
The two-part setup: containment context plus query
Container queries are not a single declaration. You need both halves:
- Declare a containment context on an ancestor element with
container-type: inline-size. This tells the browser "descendants may query this element's inline size" (its width, in horizontal writing modes). You can optionally addcontainer-nameto target it explicitly. - Write
@containerrules inside the component's styles, querying that context's dimensions much like a media query.
.card-region {
container-type: inline-size;
container-name: cardarea;
}
@container cardarea (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}An unnamed @container rule queries the nearest ancestor containment context. That's convenient until you nest containers and a rule silently binds to the wrong one — naming your containers is cheap insurance.
A worked example: the self-adapting card
Here's a complete card that stacks its image above its text when the container is narrow and switches to a side-by-side layout when there's room. The viewport width is irrelevant.
/* The wrapper establishes the containment context */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Base: stacked layout */
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card img {
width: 100%;
height: auto;
}
/* Wide container: horizontal layout */
@container card (min-width: 400px) {
.card {
flex-direction: row;
align-items: center;
}
.card img {
width: 40%;
}
}Drop .card-wrapper into a full-width main column and the card goes horizontal. Drop the identical markup into a 300px sidebar and it stacks. No changes to the component, no parent-page coordination. This is the payoff: layout decisions live with the component that owns them.
To try it yourself, put the wrapper inside a resizable element — resize: horizontal; overflow: auto; on a parent div works — and drag it back and forth across 400px while keeping the browser window fixed. The layout should flip based purely on the wrapper's width.
Container-relative units for fluid sizing
Container queries also ship with a set of units analogous to vw/vh, but relative to the query container:
cqi— 1% of the container's inline size (the most commonly useful one)cqb— 1% of the block sizecqw/cqh— width and height of the containercqmin/cqmax— the smaller or larger of the two
The classic use is fluid typography that scales with the component rather than the page:
.card h3 {
font-size: clamp(1rem, 4cqi, 1.5rem);
}The clamp() matters: raw cqi values can produce absurdly large or small text at container extremes, so bound them.
The trade-off that bites: containment changes sizing
Here's the part the demos skip. container-type: inline-size establishes layout containment on the element along the inline axis. In plain terms: the container can no longer size itself based on its own contents in that direction. If you apply it to an element that was previously shrink-wrapping its children — an inline-block, a floated box, an absolutely positioned element — it can collapse to zero width, and every query inside it evaluates against that.
The symptom is always the same: "my @container rules never match / my element disappeared." The fix is structural, not stylistic:
- Apply
container-typeto a dedicated wrapper whose width is determined by its parent (a normal block element in flow), not to the component root itself. - In DevTools, inspect the container and confirm its inline size is what you expect and isn't content-derived. Modern browser DevTools also badge elements that establish containment contexts.
A second, subtler limitation: an element cannot query its own size. The query always targets an ancestor, which is why the wrapper pattern above is idiomatic rather than optional.
Support, and what to check before shipping
Size container queries are supported in all major evergreen browsers — Chrome/Edge 105+, Safari 16+, Firefox 110+ — which covers the overwhelming majority of current traffic. That said, verify against your own analytics before publishing, and re-check a support matrix (caniuse, "CSS Container Queries (Size)") since version data ages. For older browsers, the graceful fallback is simply the base styles outside the @container block — design those to be the safe, narrow-layout default and the enhancement layers on top.
One caution on the wider family: style queries (@container style(--variant: dark)), which query custom property values rather than dimensions, have narrower support. Treat them as progressive enhancement only, and check them separately.
When to reach for this
The decision rule is simple: if a layout decision belongs to a component, use a container query; if it belongs to the page (nav collapsing, overall grid changing, print styles), keep the media query. Most mature designs end up using both, each answering the question it's actually equipped to answer. Start with one reusable component — a card, a media object, a stats block — give it a named containment wrapper, and verify the flip with a resizable test parent. Once you see a component adapt correctly in two different page contexts with zero extra code, the old viewport-only approach starts to feel like the workaround it always was.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.