038: Container Queries and Component Responsiveness
Learning outcomes
By the end, you can explain why viewport queries are sometimes the wrong dependency; establish query containers; write size container queries; use container units; choose component breakpoints; and combine container queries with Grid/Flexbox without breaking progressive enhancement.
Prerequisites and retrieval
Retrieve responsive design from 030 and media queries from 031.
Mental model: ask “how much space does this component have?”
A media query:
@media (width >= 60rem) {
.card { ... }
}
asks about the viewport.
A container query:
@container (width >= 30rem) {
.card { ... }
}
asks about an ancestor query container.
This matters when the same component appears in multiple regions.
Establish a query container
.card-region {
container-type: inline-size;
}
Now descendants can query the region's inline size.
Named container:
.sidebar-region {
container-name: sidebar;
container-type: inline-size;
}
Shorthand:
.sidebar-region {
container: sidebar / inline-size;
}
Worked example: one product card, two placements
<section class="product-region">
<article class="product-card">
<img src="shoe.jpg" alt="Blue running shoe">
<div>
<h2>Runner Pro</h2>
<p>Lightweight daily trainer.</p>
<a href="/runner-pro">View product</a>
</div>
</article>
</section>
.product-region {
container-type: inline-size;
}
.product-card {
display: grid;
gap: 1rem;
}
.product-card img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
@container (width >= 32rem) {
.product-card {
grid-template-columns: 10rem 1fr;
align-items: start;
}
.product-card img {
aspect-ratio: 1;
}
}
If this region is narrow in a sidebar, the card remains stacked even on a large desktop. In a wide main column, the card becomes horizontal.
Container query units
When an element is inside a query container, units can reference container dimensions.
.product-card h2 {
font-size: clamp(1.25rem, 4cqi, 2rem);
}
Examples include:
cqw: 1% of container widthcqh: 1% of container heightcqi: 1% of container inline sizecqb: 1% of container block sizecqmincqmax
Use bounded expressions. Pure container-relative text can become too small or too large.
Worked example: dashboard widget
.widget-zone {
container: widget / inline-size;
}
.widget {
display: grid;
gap: 1rem;
}
@container widget (width >= 24rem) {
.widget {
grid-template-columns: 1fr auto;
align-items: center;
}
}
@container widget (width >= 42rem) {
.widget__metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}
The widget responds to its actual slot, so the parent dashboard can rearrange columns without rewriting widget breakpoints.
Container queries and component ownership
Bad coupling:
.dashboard > .sidebar > .card { ... }
Better:
.sidebar {
container-type: inline-size;
}
@container (width >= 28rem) {
.card { ... }
}
The card responds to available space instead of remote ancestor names.
Style queries: conceptual introduction
Container queries can also evolve beyond size-based checks in modern CSS. Style queries can react to certain computed custom-property values in supporting environments.
A conceptual pattern:
.theme-zone {
--theme: dark;
}
Then descendants may be able to query style state depending on browser support and the exact feature used. Treat advanced style/scroll-state queries as progressive enhancement and verify current compatibility before production use.
Container query versus media query decision table
Use media queries for:
- global page layout changes based on viewport/environment;
- user preferences such as reduced motion or color scheme;
- input capabilities such as hover/pointer;
- print styles.
Use container queries for:
- reusable cards/widgets that appear in different widths;
- component-local layout changes;
- typography/spacing bounded by component size.
Use both when necessary.
Progressive enhancement
A component should work before its container query:
.card {
display: grid;
gap: 1rem;
}
Then enhance:
@supports (container-type: inline-size) {
.card-region {
container-type: inline-size;
}
@container (width >= 30rem) {
.card {
grid-template-columns: 8rem 1fr;
}
}
}
Whether you need the @supports wrapper depends on your support baseline.
Deep dive: container selection and containment
A query applies to an ancestor that establishes the relevant query container.
.product-region {
container-name: product-region;
container-type: inline-size;
}
@container product-region (width >= 34rem) {
.product-card {
grid-template-columns: 10rem 1fr;
}
}
Naming containers avoids accidentally matching a nearer unrelated container.
container-type: inline-size establishes containment needed for inline-size queries. container-type: size is stronger and can affect both axes, so do not choose it automatically.
Nested containers
.page-shell {
container: page / inline-size;
}
.sidebar-widget {
container: widget / inline-size;
}
A component can query the container that represents its own layout responsibility:
@container widget (width >= 22rem) {
.weather {
grid-template-columns: auto 1fr;
}
}
This is more stable than assuming every component should change at the same viewport width.
Container query units
Container-relative units include:
cqi— 1% of query container inline size;cqb— 1% of query container block size;cqw/cqh— width/height-based variants;cqmin/cqmax.
Example:
.card-title {
font-size: clamp(1.1rem, 4cqi, 1.8rem);
}
Still set sensible minimums/maximums. Pure proportional scaling can become unreadable in unusually small or large containers.
Style queries
Container style queries can respond to custom-property values in supporting browsers:
.card {
--density: compact;
}
@container style(--density: compact) {
.card__body {
padding: 0.75rem;
}
}
Treat this as progressive enhancement and verify the exact support required by your target browsers. Size queries are the more common baseline.
Container queries versus component state
A container query answers a layout/environment question. It should not replace application state.
Bad mental model:
/* do not infer business state from size */
@container (width < 20rem) {
.order { /* pretend order is "compact" business state */ }
}
If the application state is selected, disabled, or error, express that state through semantic HTML, attributes, or classes. Use container queries for presentation decisions caused by available space.
Debugging container queries
When a query does not fire:
- inspect which ancestor establishes a container;
- check
container-type; - confirm you are querying the intended axis;
- confirm the named container matches;
- inspect the container's actual size, not the viewport;
- check whether a nearer container is selected;
- verify the feature support baseline.
The most common mistake is asking the right query of the wrong container.
Common mistakes
- Querying an element itself instead of an eligible ancestor.
- Adding
container-typeeverywhere without understanding containment effects. - Replacing all media queries with container queries.
- Creating breakpoints based on popular device sizes.
- Using container-relative font sizes without minimum/maximum bounds.
- Writing a component that is unusable before the enhancement.
- Naming containers when an unnamed nearest container would be clearer.
Practice set
- Put the same card in a wide main region and narrow sidebar.
- Add a container query that changes from stacked to side-by-side.
- Add a container unit for a decorative gap, bounded by
clamp(). - Compare the result with a viewport media query and explain which dependency is more accurate.
- Build a reusable dashboard widget with two container breakpoints.
Recap
Media queries respond to the environment. Container queries respond to component space. They make reusable components less dependent on where they happen to be placed.
Official references
-
MDN: CSS container queries — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
-
MDN: CSS container queries — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
-
CSS Containment Level 3 — https://drafts.csswg.org/css-contain-3/
