What are container queries, and how do they differ from media queries?
Advertisement
🧩 Scenario
Architecture Walkthrough
The Problem With Viewport-Based Responsiveness
Media queries ask about the viewport, which is a global fact. Components are local: the same card may render in a three-column grid at 320px wide, in a sidebar at 260px, and as a full-width hero at 1200px, all on the same 1400px viewport.
A media query cannot distinguish those cases. @media (min-width: 1024px) is true for all three, so the card either gets its wide layout everywhere, including the sidebar where it does not fit, or it needs context-specific override classes such as card--sidebar and card--grid that the parent must remember to apply. That override approach works but pushes responsibility for a component's internal layout onto every consumer, which is exactly the coupling component architecture is meant to avoid.
How Container Queries Work
Container queries let an element respond to the size of an ancestor container rather than the viewport. There are two steps.
First, an ancestor opts into being queryable with container-type:
.card-wrapper {
container-type: inline-size;
}
Then a @container rule queries it:
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 120px 1fr;
}
}
The @container rule matches against the nearest ancestor with a container-type. The card now adapts to the space it actually has, and the same component works unchanged in a grid cell, a sidebar, or a hero.
Why the Component Cannot Query Itself
This is the constraint that trips people up: you cannot put container-type on an element and query that same element in a rule that styles it.
The reason is circularity. If a query on an element's own width could change properties that affect its width, the result would feed back into the condition and there would be no stable answer. The spec avoids this by having @container resolve against an ancestor, and by requiring containment so that the container's size does not depend on its contents.
Practically, this means the component needs a wrapper. The wrapper carries container-type and the component inside it is what the query styles. Design systems typically ship the wrapper as part of the component so consumers do not have to know about it.
inline-size Versus size Containment
container-type: inline-size applies containment on the inline axis only, making the element's width independent of its contents. Block-axis size still grows with content, which is almost always what you want. This is the value to use by default.
container-type: size applies containment on both axes, which makes height queryable but also means the element's height no longer depends on its contents. Without an explicit height, that collapses the element. Use it only when you genuinely need to query height and can set the block size explicitly.
container-type: normal is the default and makes the element queryable only for style queries, not size queries.
Note the side effect that both size values carry: applying containment means the element establishes a new containment context, which among other things creates a stacking context and affects how absolutely positioned descendants resolve. That is rarely a problem but is worth knowing when a container-type addition coincides with a layering change.
Named Containers
With nesting, the nearest ancestor container is not always the one you want. container-name lets you target a specific one:
.page { container: layout / inline-size; }
.card-wrapper { container: card / inline-size; }
@container card (min-width: 400px) { … }
@container layout (min-width: 900px) { … }
The container shorthand takes name / type. Naming containers is good practice in any component library, because it makes the query's target explicit rather than dependent on how deeply the component happens to be nested.
Container Query Units
Container queries bring their own length units, resolved against the query container rather than the viewport: cqw and cqh for one percent of the container's width and height, cqi and cqb for inline and block size, and cqmin and cqmax for the smaller and larger of the two. font-size: clamp(1rem, 5cqi, 2rem) scales type with the component's own width, which vw could never express.
Style Queries
@container style(--variant: compact) queries a custom property value on the container rather than its size. Support is narrower than for size queries, and only custom properties are queryable at present, but it points at the eventual ability to style descendants based on a container's state without adding classes.
Where Media Queries Still Belong
Container queries do not replace media queries. Media queries remain the right tool for anything genuinely global or device-level: the overall page shell and how many columns the layout has, print styles, and the non-size media features such as prefers-color-scheme, prefers-reduced-motion, prefers-contrast, hover, and pointer. Those describe the user and the device, not an element's available space, and container queries have no equivalent.
The clean division is media queries for the page and the environment, container queries for the components inside it.
Key Code Explained
/* Step 1: an ANCESTOR opts into being queryable */
.card-wrapper {
container-type: inline-size;
}
/* Step 2: query it. The rule styles the DESCENDANT, not the container. */
.card {
display: grid;
gap: 8px; /* base: stacked */
}
@container (min-width: 400px) {
.card {
grid-template-columns: 120px 1fr; /* image beside text */
gap: 16px;
}
}
@container (min-width: 600px) {
.card__excerpt {
display: block; /* only show the excerpt when there is room */
}
}
/* WRONG: an element cannot query itself */
.self-query {
container-type: inline-size;
}
@container (min-width: 400px) {
.self-query {
/* does not match — @container resolves against an ANCESTOR */
}
}
/* inline-size: width is containable, height still grows with content */
.safe {
container-type: inline-size;
}
/* size: BOTH axes contained — needs an explicit height or it collapses */
.height-queryable {
container-type: size;
height: 320px; /* required */
}
/* Named containers make the target explicit under nesting */
.page {
container: layout / inline-size; /* name / type shorthand */
}
.card-wrapper {
container: card / inline-size;
}
@container card (min-width: 400px) {
.card { grid-template-columns: 120px 1fr; }
}
@container layout (min-width: 900px) {
.page__grid { grid-template-columns: 1fr 320px; }
}
/* Container query units resolve against the CONTAINER, not the viewport */
.card__title {
font-size: clamp(1rem, 5cqi, 1.75rem);
padding-inline: 4cqi;
}
/* The same component in three contexts — one set of rules handles all */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); }
.sidebar { width: 260px; }
.hero { width: 100%; }
/* Each .card-wrapper inside these queries its OWN width. */
/* Media queries keep the jobs container queries cannot do */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
@media print {
.sidebar { display: none; }
}
The three-context block at the end is the point of the whole feature. One card component, three very different amounts of available space, one set of rules, and no card--sidebar modifier class for a parent to remember. That is the concrete improvement over media queries, and it is worth stating in exactly those terms.
The .self-query block is the mistake everybody makes once. It reads as though it should work, produces no error, and simply never matches. Knowing that @container resolves against an ancestor, and why circularity forces that, turns it from a mystery into an expected constraint that the wrapper pattern solves.
Tradeoffs
| Aspect | Media queries | Container queries |
|---|---|---|
| Responds to | Viewport and device | An ancestor container's size |
| Component reuse across contexts | Needs modifier classes | Works unchanged |
| Requires setup | None | container-type on an ancestor |
| Can query itself | N/A | No, needs a wrapper |
| Own length units | vw, vh, dvh | cqw, cqi, cqmin |
| Non-size features | prefers-*, hover, pointer, print | Style queries only, limited support |
| Best for | Page shell, environment, print | Component internals |
What Interviewers Actually Check
- Whether you can write a working
container-typeplus@containerpair - Whether you know the query targets an ancestor and can explain the circularity reason
- Whether you default to
inline-sizeand know whatsizerequires - Whether you know the
cq*units exist - Whether you can say what media queries remain the correct tool for
Follow-Up Questions
- What does containment actually mean, and how do
contain: inline-sizeandcontainer-type: inline-sizerelate? - What side effects does applying containment have on stacking contexts and absolutely positioned descendants?
- How do style queries work today, and what is the current limitation on what can be queried?
- How would you ship a component with container queries so consumers do not need to add the wrapper themselves?
- How do container query units compare to viewport units for fluid typography inside a reusable component?
Common Candidate Mistakes
- Writing
@containerrules with nocontainer-typedeclared on any ancestor, which silently never matches - Putting
container-typeon the component and then querying that same element, when@containerresolves against an ancestor to avoid circular sizing - Using
container-type: sizewithout an explicit block size, which collapses the element because its height no longer derives from its contents - Presenting container queries as a full replacement for media queries, when
prefers-reduced-motion,prefers-color-scheme,hover,pointer, andprinthave no container equivalent - Relying on the nearest-ancestor resolution under deep nesting instead of naming containers, which makes a query's target depend on where the component happens to be placed
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you write a working container query pair from memory?
- Can you explain why the wrapper rather than the component itself carries
container-type? - Can you explain the difference between
inline-sizeandsizecontainment and what each requires? - Can you name the container query length units and say what they resolve against?
- Can you say which responsive jobs media queries are still the right tool for?
Summary
Container queries let an element respond to the size of an ancestor container rather than the viewport, which resolves the mismatch at the heart of media-query-based design: components are local and reusable while viewport width is global. The same card in a three-column grid, a narrow sidebar, and a full-width hero has three very different amounts of space on one viewport, and only a container query can tell them apart.
Using them takes two steps: an ancestor declares container-type: inline-size, and a @container rule then styles descendants based on that ancestor's size. An element cannot query itself, because a query result that changed the element's own size would be circular, so components need a wrapper that carries the container-type. Prefer inline-size, which contains only the inline axis and lets height grow with content; container-type: size contains both axes and requires an explicit block size or the element collapses. container-name and the container shorthand make the query target explicit under nesting, and the cqw, cqi, cqmin family of units resolves lengths against the container, enabling genuinely component-relative fluid type.
Container queries do not replace media queries. Media queries remain correct for the page shell and for everything about the environment rather than the element: prefers-color-scheme, prefers-reduced-motion, hover, pointer, and print. The clean division is media queries for the page and the device, container queries for the components inside it.
Why can a container not query itself?
Because the query result would change the container size, which would change the query result. The spec avoids that circularity by having @container match against an ancestor container, never the element being styled.
What does container-type: inline-size actually do?
It applies inline-size containment, meaning the element size in the inline axis no longer depends on its contents. That is what makes the width queryable without circular dependency.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement