CSS Container Queries
Nesting kept component selectors in one place. Media queries still watch the viewport. Container queries answer a different question: how wide is this parent box? That is how one product card can go compact in a narrow rail and open up in a wide main column — even when the browser window never changed.
Why the viewport is not enough
A layout often places the same component in unequal slots: a 16rem sidebar rail and a fluid main area. Both slots share one viewport width, so @media (min-width: …) fires the same way for both. You need styles that key off the container that wraps the card, not the whole screen.
Make a containment context
Pick the parent that should report its size. Set container-type — usually inline-size so queries can use the parent’s inline size (width in horizontal writing modes) without forcing a full size containment on both axes:
.rail {
container-type: inline-size;
container-name: rail;
width: 16rem;
padding: 1rem;
background: #f8fafc;
}
.stage {
container-type: inline-size;
container-name: stage;
flex: 1;
min-width: 0;
padding: 1.25rem;
}
Without container-type, @container has nothing reliable to measure. container-name is optional but powerful when several ancestors are containers — you can aim at one by name.
Query the container with @container
Inside (or after) that setup, wrap rules in @container with a size condition. Descendants of the container respond when that box matches — not when the viewport does:
.product-card {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
background: #fff;
}
.product-card .media {
aspect-ratio: 16 / 10;
border-radius: 0.5rem;
background: #cbd5e1;
}
.product-card .meta {
font-size: 0.875rem;
color: #64748b;
}
@container (min-width: 28rem) {
.product-card {
grid-template-columns: 10rem 1fr;
align-items: center;
}
.product-card .title {
font-size: 1.25rem;
}
}
When .product-card lives in a narrow .rail, the single-column stack stays. Drop the same markup into a wide .stage and the two-column layout unlocks — same HTML, same viewport, different container width.
Named containers
If nested containers confuse which box is queried, pass the name after @container:
@container stage (min-width: 36rem) {
.product-card {
grid-template-columns: 12rem 1fr auto;
gap: 1.25rem;
}
.product-card .buy {
justify-self: end;
}
}
@container rail (max-width: 18rem) {
.product-card .meta {
display: none;
}
}
@container stage (…) only cares about the ancestor named stage. That keeps a rail’s tight rules from fighting a stage’s roomy rules when both wrap related markup.
Container query units
Once a containment context exists, length units like cqi (1% of the query container’s inline size) and cqw (1% of its width) let type and spacing scale with the parent, not the viewport (vi / vw):
.product-card .title {
font-size: clamp(1rem, 0.85rem + 2.5cqi, 1.5rem);
}
.product-card {
padding: clamp(0.75rem, 2.5cqi, 1.5rem);
}
Pair these with var() and clamp() from earlier Modern Toolkit lessons. Container units measure the containment context used for query — ideal for reusable widgets.
Container vs media — who does which job?
📊 Container queries vs media queries
| Question you are asking | Prefer | Typical tool |
|---|---|---|
| How wide is the browser viewport? | Page shell, nav, multi-column page | @media (min-width: …) |
| How wide is this parent around a component? | Cards, widgets, embeds in unequal slots | @container (min-width: …) after container-type |
| Do I need both? | Often yes | MQ for page; CQ for the component inside a slot |
Twin slots, one component
A small layout sketch shows the payoff — two containers, one card class:
.page {
display: flex;
gap: 1rem;
align-items: start;
}
.rail {
container-type: inline-size;
container-name: rail;
width: 16rem;
}
.stage {
container-type: inline-size;
container-name: stage;
flex: 1;
min-width: 0;
}
/* Same .product-card class in both slots — @container rules above decide layout */
You do not fork .product-card--sidebar and .product-card--main only to mirror slot width. The slot reports its size; the card listens.
Pair page media with component containers
Keep viewport queries for page decisions (collapse a site nav, switch the whole shell to one column). Keep container queries for component decisions inside whatever slot the page gave them:
@media (max-width: 40rem) {
.page {
flex-direction: column;
}
.rail {
width: auto;
}
}
@container (min-width: 28rem) {
.product-card {
grid-template-columns: 10rem 1fr;
}
}
On a small phone the page stacks; on a wide desktop the rail may still be narrow while the stage is wide — only container queries express that split cleanly.
Try it yourself
- Add
container-type: inline-sizeto a wrapper around a card; confirm DevTools shows a containment context. - Write
@container (min-width: 24rem)to switch the card from stacked to two columns. - Set
container-name: paneland retarget with@container panel (min-width: 30rem). - Size a heading with
cqiand resize only the parent (not the window) to see type move. - Leave one
@mediaon the page shell; refuse to duplicate that logic inside the card.
Common mistakes
- Writing
@containerwithoutcontainer-typeon an ancestor — nothing useful to query - Replacing every media query with container queries — page shell still belongs to
@media - Nesting three containers and wondering which width won — name them and target explicitly
- Copying viewport breakpoints (
48rem) blindly into every widget — tune to the slot, not the device catalog - Confusing nesting (
&, nested selectors) with containment — nesting organizes selectors; container queries measure boxes
Summary
- Container queries style from a parent’s size, not only the viewport
- Set
container-type(ofteninline-size); optionalcontainer-namefor clarity - Use
@container (min-width: …)/ named@container name (…)to restyle descendants - Reach for
cqi/cqwwhen lengths should track the container - Next in Modern Toolkit: feature queries (
@supports) — gate styles on browser capability
🧠 Test Your Knowledge
Test Your Knowledge
Challenge yourself with this interactive quiz and see how well you understand the topic
📝 Instructions
- Read each question carefully
- Select the best answer for each question
- You can retake the quiz as many times as you want
- Your progress will be shown at the top