Container queries — the layout primitive CSS spent 15 years missing
Media queries ask 'how big is the viewport?'. That's the wrong question for a component. Container queries ask 'how big is the slot this component was dropped into?' — which is the question any reusable component has always needed to answer. Here's the spec, the four `container-type` values, and the migration that replaces every sidebar-vs-main-content branch.
Same card, three container widths. No media queries — the breakpoints live on the parent:
.card-container {
container-type: inline-size; /* opt this parent into container queries */
container-name: card; /* optional — lets queries target by name */
}
@container card (min-width: 320px) {
.card { display: flex; align-items: center; gap: 0.75rem; }
}
@container card (min-width: 540px) {
.card .action { display: inline-flex; }
}For 15 years, responsive design meant "query the viewport, then hope the component lives in a slot that matches." A product card at the top of a full-width hero was one layout. The same card in a sidebar was a different component — or the same component with a stack of media-query-driven overrides keyed off some .sidebar .card escape hatch. Every design system had this problem. None of them solved it cleanly until CSS container queries shipped in 2023.
Container queries ask "how big is my parent?", not "how big is the viewport?" That's the question a component has always needed to answer. CSS finally has syntax for it.
Declare a parent as a query container with container-type: inline-size (or size, or scroll-state). Write breakpoints against that container with @container (min-width: 320px) { … }. The component adapts to its slot, not the viewport. Supported in all evergreen browsers since Feb 2023 (Chrome 106 / Firefox 110 / Safari 16). Cost: the container must accept inline-size containment (or full size containment for container-type: size). Use inline-size by default; only reach for size when you need height queries.
What the spec says
Container queries are defined in CSS Conditional Rules Module Level 5 (§5). They were originally drafted in CSS Containment Level 3 and later moved — the Conditional Rules draft is the current normative home.
A container query is a conditional group rule that applies styles based on the features of a query container. Query containers are established by the
container-typeproperty on their principal box.
Two ideas bundled together:
- Query container — any element with a non-default
container-typebecomes a target for@containerrules. - Conditional group rule — the queries use the same grammar as
@media, just with "container" semantics replacing "viewport" semantics.
The default is container-type: normal — elements do NOT become query containers unless you opt in. That's deliberate. Inline-size containment has a cost: the container's inline dimension can no longer depend on its descendants. The spec chose "explicit opt-in" over "pay the cost everywhere".
The four container-type values
@container (min-width: 320px) {
.card { flex-direction: row; }
}The rule of thumb:
inline-size— default choice. You can query width (the inline dimension); the container applies inline-size containment. Works for 95% of component responsiveness needs.size— you can query both width and height. Requires the container to have explicit dimensions; a size-contained box with no declared height collapses to 0.scroll-state— Chrome / Edge 133+ (Jan 2025); not yet in Firefox or Safari. Query sticky-stuck state and scroll position. Less invasive thansize; pairs withposition: stickyfor self-styling sticky headers.normal— the default; NOT a query container.
The common mistake: setting container-type: size on a wrapper with no explicit height. The wrapper vanishes. Every developer spends 20 minutes figuring out why. The fix is usually "I meant inline-size".
The migration pattern
Before container queries, a responsive product card looked like this:
/* Viewport-based — works for the page layout, fails for components */
.card { display: flex; flex-direction: column; }
@media (min-width: 768px) {
.card { flex-direction: row; }
}
/* Escape hatch for when the card is in a sidebar —
viewport is the only lever you have */
@media (min-width: 768px) and (max-width: 1100px) {
.sidebar .card { flex-direction: column; }
}Two problems:
- The component's behaviour depends on who its parent is. Breaking encapsulation.
- The same component can't live in a 320px slot on a 1440px viewport.
With container queries:
.card-slot {
container-type: inline-size;
container-name: card;
}
.card {
display: flex;
flex-direction: column;
}
@container card (min-width: 320px) {
.card { flex-direction: row; }
}Now the card adapts to its slot. Full-width hero? Horizontal. Sidebar? Column. Grid cell at 400px? Horizontal. No escape hatches. The component is self-contained.
The cqi / cqw units
Container queries ship with their own unit family — viewport-unit-like but relative to the container:
| Unit | Meaning |
|---|---|
cqw | 1% of the query container's width |
cqh | 1% of the query container's height |
cqi | 1% of the query container's inline size |
cqb | 1% of the query container's block size |
cqmin | smaller of cqi and cqb |
cqmax | larger of cqi and cqb |
Useful for fluid typography scoped to a component:
.card-slot { container-type: inline-size; }
.card-title {
font-size: clamp(1rem, 4cqi, 1.5rem);
}Title font scales from 1rem at narrow containers to 1.5rem at wide ones, regardless of viewport. Perfect for components that live at multiple container widths.
When to stick with @media
Container queries are the right answer for components. They are not a replacement for media queries, which still own two things:
- Page-level layout — "sidebar on desktop, stacked on mobile" is a viewport question. Use
@media. - User preferences —
@media (prefers-reduced-motion),@media (prefers-color-scheme: dark),@media (prefers-reduced-data). These live on the user's environment, not any container.
The rule of thumb: if the component's behaviour depends on the component's slot size, use @container. If it depends on the user's viewport or preferences, use @media.
Browser support + shipping
All evergreen browsers since Feb 2023:
- Chrome / Edge 106 (Sep 2022)
- Firefox 110 (Feb 2023)
- Safari 16 (Sep 2022)
container-type: scroll-state is newer — Chrome / Edge 133+ (Jan 2025); not yet supported in Firefox or Safari. Check caniuse if relying on it.
No polyfill is needed for the common case; older browsers degrade to the un-queried base styles. As long as your "no-query" layout is usable, the degradation is graceful.
Primary sources
- CSS Conditional Rules Level 5 — §5 Container Queries — the current normative definition.
- CSS Conditional Rules Level 5 — container-type — the value list (normal / size / inline-size / scroll-state) and their containment implications.
- CSS Values Level 4 — Container query length units —
cqw,cqh,cqi,cqb,cqmin,cqmax. - MDN — Using container queries — applied patterns and browser support.
- web.dev — CSS container queries — deployment patterns and migrations.