CSS Container Queries: Components That Adapt to Their Own Space
How container queries differ from media queries, the containment rules and gotchas, and why they finally make a component library truly reusable.
Table of contents
- The problem media queries cannot solve
- The syntax
- Named containers
- container-type has real consequences
- Container query units
- Style queries
- Progressive enhancement
- Frequently asked questions
- Do container queries replace media queries?
- Is there a performance cost?
- Why is my container query not matching?
- Can I query the number of children?
- Related reading
- References
A media query asks about the viewport. A container query asks about the element's own available space — which is the question a reusable component actually needs answered.
The problem media queries cannot solve#
A card component sits in a wide main column on one page and a narrow sidebar on another. At a 1400px viewport, a media query says "wide" in both places — but the sidebar card has 280px to work with.
The old workarounds were all bad: a --compact variant class the parent must remember to pass, or JavaScript measuring the element. Container queries make it a CSS concern.
The syntax#
Two steps. Declare containment on the parent, then query it:
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
gap: 1rem;
}
@container (width > 30rem) {
.card {
grid-template-columns: 10rem 1fr; /* image beside text */
}
}The same .card is stacked at 280px and horizontal at 600px, with no knowledge of the page layout and no props.
Named containers#
With nesting, unnamed queries resolve to the nearest container — which is often not the one you meant. Name them:
.sidebar {
container: sidebar / inline-size;
}
.main {
container: main / inline-size;
}
@container main (width > 40rem) {
.card {
/* only when inside .main */
}
}The container shorthand is name / type.
container-type has real consequences#
This is the part that surprises people. container-type establishes containment, which changes layout behaviour:
inline-size— the element's inline size no longer depends on its contents. It behaves as though it has a defined width, so an inline-block or float-based element may collapse differently.size— both dimensions are contained, which means the element loses intrinsic height. A container withcontainer-type: sizeand no explicit height collapses to zero. This catches almost everyone once.
Use inline-size unless you genuinely need to query height, which is rare.
Also note: a container cannot query itself. @container styles descendants of the container, never the container element. Wrapping is required, which is why the example above has a .card-wrapper.
Container query units#
Inside a container you get units relative to it:
.card h2 {
/* 5% of the container's inline size, clamped */
font-size: clamp(1rem, 5cqi, 2rem);
}cqw/cqh— 1% of container width / heightcqi/cqb— 1% of inline / block size (logical, so they respect writing mode)cqmin/cqmax— the smaller / larger of the two
cqi is the one to reach for: fluid typography that scales with the component rather than the viewport. A card in a sidebar gets proportionally sized text automatically.
Style queries#
Querying a custom property's value, rather than a size:
.card {
--variant: default;
}
.card.featured {
--variant: featured;
}
@container style(--variant: featured) {
.card-title {
font-size: 1.5rem;
}
}This is supported for custom properties in current browsers. It is a way to pass a variant down a subtree without threading a class through every element — genuinely useful in a design system, though the ergonomics are still settling.
Progressive enhancement#
Container queries are supported in all current browsers, but if you need a fallback, write the narrow layout as the base and enhance:
.card {
display: grid;
} /* stacked — works everywhere */
@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
}
@container (width > 30rem) {
.card {
grid-template-columns: 10rem 1fr;
}
}
}Mobile-first still applies: the unqueried styles should be the narrow case.
Frequently asked questions#
Do container queries replace media queries?#
No. Media queries remain right for page-level layout, and for things that genuinely are viewport concerns — print styles, prefers-reduced-motion, prefers-color-scheme. Container queries are for components.
Is there a performance cost?#
Containment generally helps — it lets the browser skip layout work outside the container. Very deeply nested containers add some cost, but it is not a practical concern at normal depths.
Why is my container query not matching?#
Three usual causes: the query targets the container itself rather than a descendant; container-type is missing on the parent; or an unnamed query is resolving to a nearer container than intended. Name your containers.
Can I query the number of children?#
No. That is a layout-driven question CSS cannot answer; grid-template-columns: repeat(auto-fit, minmax(...)) solves most cases where you would want to.