Container queries, and what they fixed
The previous lessons all shared one limitation: a media query knows the size of the window, and a component does not care about the window. It cares how much space it has.
Put the same card in a 300px sidebar and in a 900px main column. The viewport is identical for both, so a media query cannot tell them apart. For twenty years the workarounds were a modifier class you had to remember to apply, or JavaScript measuring elements and adding classes on resize.
Container queries fix it properly, and they are the biggest change to CSS layout since Grid.
The two parts
.card-area {
container-type: inline-size; /* 1. declare a container */
}
.card {
display: grid;
gap: 0.75rem;
}
@container (width >= 30rem) { /* 2. query it */
.card {
grid-template-columns: 12rem 1fr;
align-items: start;
}
}
Now the card becomes two-column when its own container is at least 30rem wide — in the main column but not in the sidebar, at the same viewport width. Drop the component anywhere and it adapts to where it landed.
You must declare container-type on an ancestor, or @container matches nothing.
That is the commonest reason it appears not to work, and it fails silently as always.
container-type values
container-type: inline-size; /* query the inline size (width in English) */
container-type: size; /* query both axes */
container-type: normal; /* not a size container — only style queries */
Use inline-size almost always. It is the one that is safe, because of the next
section.
The containment cost
Declaring a size container applies containment on that axis, and this is the trap worth understanding before you use it.
container-type: size — querying both axes — means the container can no longer be
sized by its children's height. Its height collapses unless you give it one. That is
inherent: the children's styles depend on the height, and the height cannot depend on
the children, or it is circular.
inline-size only contains the inline axis, and blocks already get their width from
their parent rather than their content, so nothing breaks. This is why inline-size
is the default choice and size is a special case you reach for knowingly, for
something with a fixed height.
There is a second consequence either way: a size container cannot be queried by
itself. @container inside .card queries .card's container, not .card. So the
container and the thing being styled must be different elements — hence the
.card-area wrapper above. Trying to put container-type on the card and query the
card is the second commonest mistake.
Named containers
With nested containers, say which one you mean:
.sidebar { container-type: inline-size; container-name: sidebar; }
.card-area { container-type: inline-size; container-name: card; }
@container card (width >= 30rem) { .card { … } }
@container sidebar (width >= 20rem) { .card__meta { … } }
The shorthand:
.card-area { container: card / inline-size; }
An unnamed @container query matches the nearest ancestor container, which is
usually right and occasionally surprising. Name them once you have more than one.
Container query units
Units relative to the container rather than the viewport:
.card__title {
font-size: clamp(1rem, 5cqi, 1.5rem);
padding: 2cqi;
}
| Unit | Relative to |
|---|---|
cqi |
the container's inline size |
cqb |
the container's block size |
cqw / cqh |
its width / height |
cqmin / cqmax |
the smaller / larger of the two |
cqi is the one to use. It is vw for components: a title that scales with its
card rather than with the window, so the card in the sidebar has proportionally
smaller text without a single query.
The same accessibility rule from the fluid-type lesson applies — keep a rem term in
the middle of the clamp(), because cqi alone ignores the visitor's font-size
setting just as vw does.
When to use which
Not a replacement for media queries. They answer different questions:
| Question | Tool |
|---|---|
| How big is the window? | media query |
| How big is this component's space? | container query |
| Is the whole page layout changing? | media query |
| Is one component adapting to its slot? | container query |
| What does the user prefer? | media query |
| Is it a phone or a mouse? | media query |
Page skeleton: media query. Components inside it: container query. That division holds up well in practice, and it means a component's CSS no longer contains any knowledge of the page's breakpoints — which is the real win. A card that works in the sidebar, the main column, a modal and a footer, with no variants, is a component you can move without thinking.
A real example
A product card that is stacked when narrow and side-by-side when it has room, used in two places at once:
/* the page: media query */
.layout {
display: grid;
gap: 2rem;
}
@media (width >= 60rem) {
.layout { grid-template-columns: 1fr 18rem; }
}
/* both slots are containers */
.layout > * { container-type: inline-size; }
/* the component: container query, no page knowledge at all */
.product {
display: grid;
gap: 0.75rem;
}
.product img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
.product h3 { font-size: clamp(1rem, 0.9rem + 2cqi, 1.375rem); }
@container (width >= 26rem) {
.product {
grid-template-columns: 10rem 1fr;
align-items: start;
}
.product img { aspect-ratio: 1; }
}
At a 1200px viewport the main column is wide, so those products are side-by-side; the
18rem sidebar is narrow, so the same markup is stacked. One component, no variants, and
nothing in .product refers to 60rem.
Style queries
The other half of @container, and newer:
.card { container-type: normal; }
@container style(--variant: featured) {
.card__title { font-size: 1.5rem; }
}
Queries a custom property value on the container rather than its size. Useful for
theming a component from a parent without adding classes at every level. Support is
still arriving for the general case — check caniuse.com — so treat this as worth
knowing rather than worth relying on.
Support and fallback
Container queries are supported in all current browsers. For an older one, the sensible fallback is that the query simply does not match, so you get the base styles — which, if you have written mobile-first, is the stacked single-column version. That degrades acceptably with no effort.
If you need to be explicit:
@supports (container-type: inline-size) {
/* the enhanced version */
}
Check your work
What a media query cannot know. How much space a component has.
The two parts. container-type on an ancestor, then @container.
The commonest reason it does nothing. No container-type declared.
Why inline-size rather than size. size contains both axes, so the container's
height collapses; blocks already take their width from the parent, so inline-size
breaks nothing.
Why the container must be a different element. A container cannot be queried by itself, so a wrapper is required.
What an unnamed @container matches. The nearest ancestor container.
What cqi is. vw for components — relative to the container's inline size.
The accessibility rule that carries over. Keep a rem term in a clamp(); cqi
ignores the font-size setting just as vw does.
The division of labour. Page skeleton by media query, components by container query.
The real win. A component's CSS contains no knowledge of the page's breakpoints.
What a style query queries. A custom property value on the container.
What the fallback is in an old browser. The query does not match, so you get the mobile-first base styles.
Practice
- Put the same card in a 300px sidebar and a 900px main column and try to style them differently with a media query. Confirm you cannot.
- Add
container-type: inline-sizeto both slots and a@containerquery. Confirm they now differ at one viewport width. - Remove the
container-typeand confirm the query silently stops matching. - Put
container-typeon the card itself and query the card. Explain what happens. - Set
container-type: sizeon a container whose height comes from its content. Measure the height before and after. - Nest two containers and use an unnamed query. Work out which one it matched.
- Name them both and target the outer one deliberately.
- Use
5cqifor a title's font size and resize the container, not the window. - Compare
5cqiwith5vwfor the same card in the sidebar. - Set the browser's default font size to 24px and check whether your
cqititle responded. Then add aremterm. - Build the product-card example and confirm one component serves both slots with no variant class.
- Search your own CSS for a
.card--narrow-style modifier and replace it with a container query. - Wrap an enhancement in
@supports (container-type: inline-size). - Look up container query support on caniuse.com and note the oldest version that has it.
Official documentation
- MDN — CSS container queries — The full guide, including named containers and the containment consequences.
- MDN — container-type — What each value contains, and why
sizecollapses a height. - MDN — Container query length units —
cqi,cqb,cqminand the rest. - caniuse — CSS container queries — Current support, for deciding whether you need a fallback.
Next: honouring what the visitor has already told their device.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship