For most of CSS’s history, responsive design has meant one thing: media queries tied to the viewport. That works well for page-level layout shifts, but it breaks down the moment you build reusable components that get dropped into different contexts — a sidebar, a main column, a modal, a grid cell. A card component that looks great at 400px in a three-column grid might look cramped at the same 400px inside a narrow sidebar, even though the viewport width hasn’t changed at all. Container queries solve this by letting an element respond to the size of its containing element instead of the size of the browser window. Support has matured across all major browsers (Chrome, Edge, Safari, and Firefox all ship it as of recent versions), so it’s now a practical tool for production layouts rather than an experimental feature.
The Problem Container Queries Solve
Imagine a product card component used in two places: a 3-column grid on desktop and a single-column sidebar “recommended items” list. With media queries alone, you can only react to the browser viewport width, not to how much space the card itself has. The result is usually one of two workarounds: duplicating the component with different class names per context, or writing brittle CSS that guesses at viewport breakpoints and hopes the component’s actual rendered width lines up. Neither scales cleanly as a design system grows.
Container queries flip the model. You mark an element as a containment context, and any descendant can query that container’s size directly — regardless of what the browser viewport is doing.
Basic Syntax
Setting up a container query takes two steps: define a containment context on a parent, then write an @container rule that targets descendants of that container.
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@container card (min-width: 400px) {
.card {
flex-direction: row;
align-items: center;
}
.card img {
width: 40%;
}
}
container-type: inline-size tells the browser to track the element’s inline-axis size (width, in a horizontal writing mode) and establish a containment context. container-name is optional but recommended once you have more than one nested container, since it disambiguates which container an @container rule is targeting. Without a name, the query matches the nearest ancestor with a containment context, which can get confusing in deeply nested markup.
Container Query Units
Alongside the @container at-rule, CSS also introduced container query length units: cqw, cqh, cqi, cqb, cqmin, and cqmax. These behave like viewport units (vw, vh) but are relative to the query container instead of the viewport.
.card-title {
/* scales with the container's inline size, clamped to a sane range */
font-size: clamp(1rem, 4cqi, 1.5rem);
}
This is particularly useful for typography and spacing inside components that need to feel proportional to their container rather than fixed at one pixel value or tied to the viewport.
A Real Layout Example
Here’s a slightly more complete example: a card grid where cards reflow their internal layout based on how much room the grid track gives them, not on the page’s overall width.
<div class="grid">
<div class="card-wrapper">
<article class="card">
<img src="thumb.jpg" alt="" />
<div class="card-body">
<h3 class="card-title">Container Queries</h3>
<p>Layout that responds to its own box, not the viewport.</p>
</div>
</article>
</div>
</div>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.card-wrapper {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
@container (min-width: 300px) {
.card {
flex-direction: row;
}
.card img {
width: 40%;
object-fit: cover;
}
}
Resize the browser and, thanks to auto-fill and minmax, the grid tracks grow and shrink. Once a track crosses 300px, its card switches from a stacked layout to a side-by-side layout — entirely independent of the viewport width. Put this same .card-wrapper in a 250px sidebar and it will always render stacked, because its container never reaches 300px there, even on a huge monitor.
Style Queries: Beyond Size
A newer, less widely supported addition is container style queries, which let you query a container’s computed custom property values rather than its dimensions:
.theme-dark {
--card-theme: dark;
}
@container style(--card-theme: dark) {
.card {
background: #1a1a1a;
color: #f5f5f5;
}
}
Style queries are a good tool to know about, but check current browser support before relying on them in production — size-based container queries have much broader support and should be your default starting point.
Fallbacks and Progressive Enhancement
Because container queries are additive — they layer on top of your base styles — the safest pattern is to write sensible default styles first, then use @container rules to enhance the layout when space allows. For the rare case where you need to detect support explicitly, use @supports:
@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
}
}
Browsers that don’t understand container-type or @container simply ignore those rules and keep rendering your base (stacked) layout, so there’s rarely a need for JavaScript-based feature detection.
Common Pitfalls
- Querying the element itself: an element cannot be queried by its own container-type; the containment context must be on an ancestor, and the query styles apply to that ancestor’s descendants.
- Forgetting
container-namein nested contexts: if you nest containers (a card inside a sidebar that’s also a container), an unnamed@containerquery matches the nearest one, which may not be the one you intended. - Using
container-type: sizeunnecessarily: this tracks both axes and forces the element out of normal block-level sizing behavior (it needs an explicit height). Stick toinline-sizeunless you specifically need block-axis queries.
Conclusion
Container queries close a real gap in CSS: the ability to build components that adapt to their own context instead of the page’s. For design systems and component libraries in particular, this means fewer layout variants to maintain and fewer surprises when a component gets reused somewhere the original author didn’t anticipate. Start small — pick one reusable component, mark its wrapper as a containment context, and add an @container rule for the breakpoint where its layout genuinely needs to change. Once it clicks, you’ll find yourself reaching for it any time a component’s ideal layout depends on “how much room do I actually have” rather than “how wide is the browser.”