Container vs Media Queries: Choosing the Right Reference Box
The decision you are actually making
You have a card component that renders in three places: a narrow sidebar, a two-up grid, and full-bleed at the top of an article. You write @media (min-width: 768px) { .card { flex-direction: row; } } and it looks right in the article, wrong in the sidebar, and accidentally correct in the grid on tablets only. Nothing about the syntax is broken. The rule is measuring the wrong box. This page is about that one choice — which box a responsive rule should measure — and it sits inside Container Query Syntax Basics, part of Mastering Container Queries & Responsive Layouts. The neighbouring pages cover how to declare containers and how to scale content inside them; this one only helps you pick the at-rule.
Why the reference box matters more than the syntax
@media and @container are written almost identically and behave identically in the cascade. Neither adds specificity, both wrap ordinary declaration blocks, and both re-evaluate when their subject changes size. The entire difference is the subject. @media (min-width: 768px) resolves against the viewport, a single global value shared by every element on the page. @container (min-width: 400px) resolves against the nearest ancestor that declared itself a query container, a local value that differs per instance of the component.
That distinction produces a practical rule. A viewport measurement is a proxy for available space — it happens to correlate with how wide your card is, right up until someone puts the card somewhere new. A container measurement is the available space, so it stays correct through every refactor, every CMS slot, and every reuse in a design system. Whenever a rule's real intent is "when this component has room", the viewport is a guess and the container is the answer.
The inverse also holds, and it is the part people skip. Several things a responsive design needs to know are genuinely global and are not knowable from a box at all: whether the user asked for reduced motion, whether the primary input is a coarse fingertip or a fine mouse, the display's colour scheme, resolution, orientation, and whether the page is being printed. Those live in @media permanently. There is no container-level equivalent and there will not be one, because they describe the user and the device rather than a layout.
The third option — measuring elements in JavaScript with ResizeObserver and toggling classes — still works and still has a legitimate niche, but it reads layout and then writes styles, which costs an extra frame and can produce a visible snap on first paint. It also needs teardown per component. For anything expressible as a size condition, both at-rules resolve inside the engine's own layout pass, which is why the declarative options should be exhausted first.
Complete working implementation
The document below is the honest answer to "which one?", because it uses both. The page frame is a viewport decision: a sidebar only exists when the window is wide enough to hold one. The card is a container decision: the same markup appears in the narrow aside and in the wide main column, and each instance lays itself out from the width it was handed. Save it, open it, and resize — then note that the two cards diverge at different moments, which a viewport-only stylesheet cannot express.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Viewport frame, container components</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 0; padding: 1.5rem; }
/* --- Page frame: a VIEWPORT decision. --------------------------------
Whether the page has room for a second column is a property of the
window, not of any component, so @media is the correct tool here. */
.page {
display: grid;
grid-template-columns: 1fr; /* single column by default */
gap: 1.5rem;
max-inline-size: 70rem;
margin-inline: auto;
}
@media (min-width: 60rem) {
.page { grid-template-columns: 18rem 1fr; }
}
/* --- Component slots: opt each one in as a query container. ----------
Only wrappers become containers; the card itself stays queryable. */
.slot { container-type: inline-size; }
/* --- Card baseline: the narrow layout, written first and unconditional.
Every browser gets this, including engines that ignore @container. */
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
border: 1px solid #7aa2ff;
border-radius: 12px;
}
.card__thumb {
inline-size: 100%;
aspect-ratio: 16 / 9;
border-radius: 8px;
background: #7aa2ff33;
flex: none;
}
.card__body > * { margin: 0 0 0.4rem; }
/* --- Card refinement: a CONTAINER decision. --------------------------
"Is there room for a side-by-side layout?" is a question about this
card's own box. Both cards below share this one rule and reach the
threshold at different window sizes, because their slots differ. */
@container (min-width: 26rem) {
.card { flex-direction: row; align-items: flex-start; }
.card__thumb { inline-size: 9rem; }
}
/* --- A media feature with NO container equivalent. -------------------
Pointer type describes the input device, not the layout, so it can
only ever be asked of the environment. */
@media (pointer: coarse) {
.card__action { min-block-size: 2.75rem; padding-inline: 1rem; }
}
</style>
</head>
<body>
<div class="page">
<aside class="slot">
<article class="card">
<div class="card__thumb"></div>
<div class="card__body">
<h2>Sidebar instance</h2>
<p>Stays stacked: its slot rarely reaches 26rem.</p>
<button class="card__action" type="button">Open</button>
</div>
</article>
</aside>
<main class="slot">
<article class="card">
<div class="card__thumb"></div>
<div class="card__body">
<h2>Main column instance</h2>
<p>Identical markup and CSS, but this slot is wide, so it goes side by side much sooner.</p>
<button class="card__action" type="button">Open</button>
</div>
</article>
</main>
</div>
</body>
</html>
Key technique callout
The trick that makes the file above work is that the two at-rules are layered rather than chosen between. The unconditional .card block is the narrow layout and needs no query at all; it is what an old engine, a print stylesheet, and a very small slot all render. @container then refines that baseline per instance, and @media refines it per environment. Because neither at-rule contributes specificity, nothing here depends on cascade tricks — the later rule wins, and "later" is under your direct control in the source order.
The second half of the technique is choosing the threshold. A viewport breakpoint like 768px is a device-shaped number inherited from hardware that no longer exists. A container threshold is a content-shaped number: measure the point at which the thumbnail plus a readable text column stops fitting, and use that. In the example it is 26rem, and it means something you can state in a sentence — "below this, the two columns would each be too narrow to read". Thresholds you can justify in words survive redesigns; magic device widths do not.
Variation or extension
The clearest way to feel the division of labour is to look at conditions that only one at-rule can express. Container queries have no access to anything outside the box they measure, so all of the following stay in @media regardless of how container-driven your system becomes.
/* Environment and user preference: @media only. */
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
}
@media (prefers-color-scheme: dark) {
.card { border-color: #7aa2ff; background: #11141c; color: #e8ecf4; }
}
@media print {
.card { border: 1px solid #000; break-inside: avoid; }
}
/* Available space: @container, with the modern range syntax that both
at-rules share. This reads as 26rem <= width < 48rem. */
@container (26rem <= width < 48rem) {
.card__body p { font-size: 0.95rem; }
}
/* Orientation is a viewport concept; a container has an aspect ratio
instead, and only when its type contains the block axis. */
@media (orientation: landscape) {
.page { align-items: start; }
}
Range syntax (width < 48rem, 26rem <= width < 48rem) is worth adopting in both at-rules. It removes the off-by-one seam that min-width/max-width pairs create at shared breakpoints and reads closer to the sentence you would say out loud. Note also that inside @container you write the bare feature name width, not min-width, when using the range form.
Typography is the one area where the choice is genuinely contested, because text can scale continuously instead of stepping at a threshold — the tradeoffs there are laid out in clamp() versus media query typography, and the container-relative version of the same idea uses the length units described in container query units cqi and cqb. For input-related conditions specifically, see pointer and hover media queries.
Quick decision table
| The question your rule is really asking | Use | Why |
|---|---|---|
| Does the window have room for a sidebar or a wider page grid? | @media | The page frame has exactly one instance and one reference box. |
| Does this instance of the component have room to go side by side? | @container | Correct in every slot, including ones that do not exist yet. |
| Is the user on a touch device, in dark mode, or asking for less motion? | @media | Describes the environment; no container can report it. |
| Should this text scale smoothly rather than jump at a threshold? | Neither — use clamp() | A continuous function beats a step when nothing is reflowing. |
Browser support note
Both at-rules are safe to ship together. Size container queries reached all four major engines with Chrome 105, Edge 105, Safari 16.0, and Firefox 110, putting them at baseline since 2023; media queries need no discussion. Range syntax (width < 48rem) landed alongside container queries in Chrome and Edge 104, Firefox 102, and Safari 16.4, so pair it with the range form only when your floor is at or above those versions. Because the layered pattern above keeps the narrow layout unconditional, an engine that understands neither @container nor range syntax simply drops those blocks and renders the stacked card, which is a correct result rather than a broken one. If you need an explicit gate, feature detection with @supports covers the test to use.
FAQ
Do container queries replace media queries?
No. They answer different questions. @container asks how much space a component was given, while @media asks about the viewport and the device, including preferences like reduced motion and pointer type that no container can report.
Can the same element be styled by both @media and @container? Yes. The two at-rules evaluate independently and neither adds specificity, so the declaration that wins is simply the one written later or in a higher cascade layer. Keep the viewport rule as the baseline and let the container rule refine it.
Which one should I reach for first when building a component?
Start with @container if the component can appear in more than one slot, because it stays correct wherever you drop it. Reach for @media only for page-level frames and for device or user-preference conditions.
Why did switching a media query to a container query change nothing?
Almost always because no ancestor declares container-type, so the query has no container to resolve against and never matches. A container query needs an ancestor that has opted in; the viewport is not a fallback container.
Related
- Container Query Syntax Basics — the parent guide for the full
@containerreference. - Choosing container-type: inline-size, size or normal — once you have chosen
@container, this is the next decision. - Nesting and Naming Container Queries — pointing a query at the ancestor you meant.
- How to Use Container Queries in Production — rolling the decision out across an existing codebase.
- Optimizing CSS Animations for 60fps — cross-area: the same reference-box thinking applied to motion work.
Related articles
More pages in the same section.