A card looks fine in the main content column. Move it into a sidebar on the same desktop screen and its image pushes the text into an unusably narrow strip. The viewport has not changed, so the media query that selected the horizontal layout has not changed either.
A container size query solves that mismatch: it lets a descendant change styles according to the available size of an eligible ancestor, not just the viewport. The important detail is eligible ancestor. A container query does not automatically measure the element being styled, and it does not necessarily measure the immediate parent.
This guide builds one reusable card, checks it in two columns at the same viewport width, and follows the failure cases that are easy to miss when adopting @container.
Viewport breakpoints and component breakpoints answer different questions
Consider this rule:
.product-card {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 800px) {
.product-card {
grid-template-columns: 12rem minmax(0, 1fr);
}
}
At a 1200px viewport, every matching card gets the two-column layout, including one placed inside a 300px sidebar. The rule knows nothing about how much space that particular card received.
A media query remains useful for page-level decisions such as switching a site navigation layout or responding to prefers-reduced-motion. A container query is a better fit when a reusable component must react to the width of a dashboard panel, sidebar, modal, or grid cell.
| Question | Relevant tool |
|---|---|
| Is the browser viewport wide enough for the site navigation? | @media |
| Is this card's containing region wide enough for an image beside its text? | @container |
| Can the grid fit another column without a discrete breakpoint? | repeat(auto-fit, minmax(...)), when intrinsic Grid layout is sufficient |
The query is not a replacement for Grid or Flexbox. It decides when a component should switch between layouts; Grid and Flexbox still perform the layout.
Build the same card in a main column and a sidebar
Save the following as container-card.html and open it in a browser that supports container size queries. It contains no framework or JavaScript dependency.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Container query reproduction</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font: 16px/1.5 system-ui, sans-serif; }
.layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: 16px;
}
.slot {
container: card-slot / inline-size;
min-width: 0;
}
.product-card {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px;
border: 1px solid #777;
padding: 16px;
border-radius: 12px;
}
.product-card__visual {
background: #dbeafe;
min-height: 120px;
border-radius: 8px;
}
.product-card__body { min-width: 0; }
.product-card h2 { margin: 0 0 8px; font-size: 1.25rem; }
.product-card p { margin: 0; overflow-wrap: anywhere; }
@container card-slot (min-width: 34rem) {
.product-card {
grid-template-columns: 12rem minmax(0, 1fr);
align-items: center;
}
}
/* Page-level columns still depend on the viewport. */
@media (max-width: 42rem) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
</head>
<body>
<div class="layout">
<section class="slot" id="main-slot" aria-label="Main results">
<article class="product-card" id="main-card">
<div class="product-card__visual" aria-hidden="true"></div>
<div class="product-card__body">
<h2>Reusable product card</h2>
<p>Designed for a main column or a sidebar.</p>
</div>
</article>
</section>
<aside class="slot" id="side-slot" aria-label="Sidebar results">
<article class="product-card" id="side-card">
<div class="product-card__visual" aria-hidden="true"></div>
<div class="product-card__body">
<h2>Reusable product card</h2>
<p>Designed for a main column or a sidebar.</p>
</div>
</article>
</aside>
</div>
</body>
</html>
There are four decisions worth noticing.
- The slot is the query container.
container: card-slot / inline-sizeis shorthand forcontainer-name: card-slotandcontainer-type: inline-size. The card is a descendant of that slot, so its CSS can query the slot. - The narrow layout comes first. One grid column works even when the query is unsupported or the slot is small. The two-column layout is an enhancement for containers with at least
34remof content-box width. This threshold is a layout decision, not a device category. - The grid children can shrink.
minmax(0, 1fr),min-width: 0and text wrapping handle different constraints. Container queries cannot fix a long, unbreakable string or an intrinsic minimum-width problem by themselves. The box model and overflow debugging guide examines those failures separately. - The outer page grid also needs a viewport breakpoint. Below
42rem, the main column and sidebar stack instead of squeezing the sidebar card into an unusably narrow slot. That is a page-level decision for@media; each card still switches its internal layout with@container.
The example uses a decorative rectangle rather than a real product image, so image loading does not affect the test. In a production component, supply meaningful alt text for informative images rather than hiding them from assistive technology.
Verify that the container, not the viewport, controls the card
In a headless Chromium 144 reproduction at a 1200px viewport, the layout above produced these measurements:
| State | Main slot | Sidebar slot | Main card | Sidebar card |
|---|---|---|---|---|
| Original layout | 757px | 379px | Two columns | One column |
| Layout constrained to 660px, viewport unchanged | 429px | 215px | One column | One column |
The slot widths above are rounded border-box measurements; the query itself compares the container's content box. Here, the slot has no padding or border, so that distinction does not change which rule matches. These are observations from this small fixture, not performance measurements or a cross-browser compatibility test.
To reproduce the second row without changing the browser window, open DevTools and temporarily add max-width: 660px to .layout while keeping the viewport at 1200px. The main card switches to one column. Remove the declaration and it returns to two columns. This test changes the available container width without activating the viewport media query.
The page-level breakpoint was checked independently in Chromium 144 at viewport widths of 320, 390, 660, 672, 673, and 1200 CSS pixels. At 672px and below the outer columns stack, and neither card nor the document overflows horizontally in this fixture. Above 672px, the outer layout uses two columns, while the cards still respond to their respective container widths. The test does not establish a general guarantee for different text lengths, font settings, or component content.
Two further checks distinguish a working query from a misleading CSS rule:
- Remove
container: card-slot / inline-sizefrom.slot. Neither card satisfies the named size query; both keep the narrow default, even though the main slot is wide. - Try writing a query intended to change
.slotitself. A container's size query styles descendants, not the queried element. Put the query container on an ancestor wrapper if the element you need to restyle is currently the container.
This explains why @container is not simply @media with a different keyword. The component needs a queryable ancestor.
Which container does the browser actually measure?
With an unnamed size query, the browser selects the nearest ancestor eligible for the requested size features. That may be a different ancestor than the one you expected in a nested component tree.
For example:
.dashboard-panel {
container-type: inline-size;
}
.card-shell {
container-type: inline-size;
}
/* For .card inside .card-shell, this queries .card-shell,
not the more distant .dashboard-panel. */
@container (min-width: 30rem) {
.card { /* wider layout */ }
}
Give containers names when the relationship needs to be explicit:
.dashboard-panel {
container: dashboard / inline-size;
}
.card-shell {
container: card-slot / inline-size;
}
@container dashboard (min-width: 50rem) {
.card { /* condition uses the named dashboard ancestor */ }
}
A name does not make any element queryable for size. The named ancestor still needs a compatible container-type, and the styled element must still be its descendant.
inline-size is usually a reasonable starting point for width-driven components. It queries the logical inline axis, which corresponds to width in common horizontal writing modes. size makes both axes available for size queries but also imposes stronger size containment. An element whose block size relies on its contents can behave unexpectedly under that constraint. Do not select size just because it sounds more complete.
The MDN container queries guide documents the sizing modes and naming rules, and the @container reference describes the condition syntax.
Four reasons a container query appears not to work
| Symptom | What to inspect | Likely correction |
|---|---|---|
| The wide rule never applies | Check container-type and the element's ancestors | Put inline-size on an ancestor, not only the card |
| The wrong panel controls the rule | Inspect nested eligible containers and their names | Use a specific container-name where appropriate |
| The measured width seems different | Compare content-box width with outer dimensions and padding | Choose the threshold using the queried content box |
| The rule matches but the text overflows | Inspect Grid/Flex minimum sizes and long tokens | Fix shrinking and wrapping independently |
In Chromium-based DevTools, inspect the card in the Elements panel, find the matching @container rule, and follow its link to the queried ancestor. The container badge can also display the relationship between a query container and its descendants. Google's container query debugging instructions show the inspection workflow.
Choose a breakpoint from the component's minimum usable layout width, not from a familiar phone or tablet width. For this example, the horizontal version reserves 12rem for the visual, plus the card's padding, gap, and a usable width for text. If your real content is longer or localized, retest and adjust the threshold.
Container units are useful, but not a substitute for constraints
Container-relative units let descendants scale with a size query container:
1cqiequals 1% of its inline size.1cqwequals 1% of its width.cqbandcqhrefer to block size and height.cqminandcqmaxselect the smaller or larger of the relevant logical dimensions.
For example:
.card-title {
font-size: clamp(1.125rem, 1rem + 1cqi, 1.5rem);
}
This sets a bounded font size that responds to an eligible query container's inline dimension. The clamp() limits still matter: fluid text without upper and lower bounds may become too small or too large.
There is a subtle fallback. If no eligible size container is found for an axis, the corresponding container-relative unit resolves using a small viewport unit for that axis. It does not simply become zero. The MDN length reference documents this behavior.
That fallback is another reason to inspect the ancestor chain when typography seems to react to the viewport instead of its intended panel.
Do you still need media queries or JavaScript?
Yes, depending on the question.
Use @media for global viewport conditions and user preferences. Use intrinsic Grid or Flexbox layout when its normal wrapping behavior already handles the job. Use @container when a component needs a conditional style change tied to an ancestor's size.
Container queries also include capabilities beyond size queries, including style queries. The MDN size and style query guide distinguishes them. Do not assume that a browser supporting size queries also supports querying arbitrary CSS declarations with style(): support for style features is more limited, with custom-property queries being the interoperable focus of current guidance.
For a component library targeting older browsers or embedded webviews, test its actual support matrix. A narrow-first layout gives a usable fallback in environments that ignore the enhancement. Feature detection such as CSS.supports('container-type', 'inline-size') checks whether the property value is recognized; it is not a substitute for exercising the component's real layout and relevant query syntax.
The decision matters in a reusable design system: the component should expose a clear contract about the space it needs rather than silently depending on a particular page template.
Before adopting a query, put the same component in a wide content area, a narrow sidebar, and a nested panel. Keep the viewport fixed while resizing the surrounding region. If each instance switches only when its own usable space crosses the intended threshold, you have tested the behavior that viewport media queries could not express.
