038: Container Queries and Component Responsiveness
Learning outcomes
By the end of this lesson, you should be able to explain when a viewport query is the wrong dependency, establish query containers, write size container queries, use container-relative units, choose breakpoints for a component, and combine container queries with Grid or Flexbox without undermining progressive enhancement.
Prerequisites and retrieval
Retrieve the responsive-design material from 030 and the media-query material from 031 before continuing. Those lessons describe page-level responsiveness; this lesson applies a similar idea at the component boundary.
Mental model: ask “how much space does this component have?”
When a component changes layout, the viewport is not always the thing that determines whether the change is useful. A media query asks about the viewport:
@media (width >= 60rem) {
.card { ... }
}
A container query asks about an ancestor that has been designated as a query container:
@container (width >= 30rem) {
.card { ... }
}
That distinction is the foundation of component responsiveness. The same card may be placed in a wide main column and a narrow sidebar. A viewport-wide breakpoint cannot tell those two cards apart, but each card's container can report the space it actually has.
Establish a query container
Give an ancestor an inline-size query container with container-type:
.card-region {
container-type: inline-size;
}
Descendants can now query the region's inline size. In a typical horizontal writing mode, that is the width, but the logical property is the useful part because it also works with writing-mode changes.
You can name a container when a component should explicitly target a particular ancestor:
.sidebar-region {
container-name: sidebar;
container-type: inline-size;
}
The shorthand combines both declarations:
.sidebar-region {
container: sidebar / inline-size;
}
Worked example: one product card, two placements
Here is a product card that can be placed in either a main region or a sidebar:
<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>
The region owns the query, while the card owns its own layout rules:
.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;
}
}
The base rule keeps the card stacked. Once the region reaches 32rem, the card becomes horizontal and its image becomes square. If the region is narrow in a sidebar, it stays stacked even on a large desktop. If the main column is wide enough, the same component changes layout there. The breakpoint belongs to the card's available space, not to a particular device width.
Container query units
Inside a query container, container-relative units let a value refer to that container's dimensions rather than the viewport. For example:
.product-card h2 {
font-size: clamp(1.25rem, 4cqi, 2rem);
}
Common units include:
cqw: 1% of the container widthcqh: 1% of the container heightcqi: 1% of the container inline sizecqb: 1% of the container block sizecqmincqmax
Keep container-relative values bounded. A purely container-relative font size can become uncomfortably small in a cramped slot or excessively large in a very wide one; clamp() gives the value usable limits.
Worked example: dashboard widget
This widget uses a named container and two component-local breakpoints:
.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);
}
}
At 24rem, the widget can place its main content and secondary content on one row. At 42rem, its metrics get three columns as well. The widget responds to the slot it occupies, so a parent dashboard can rearrange its columns without forcing the widget to know about the dashboard's viewport breakpoints.
Container queries and component ownership
A selector that depends on a distant page structure couples the card to its current placement:
.dashboard > .sidebar > .card { ... }
Let the layout region establish the container instead:
.sidebar {
container-type: inline-size;
}
@container (width >= 28rem) {
.card { ... }
}
The card now responds to the space available to it, not to the names and depth of remote ancestors. That makes the component easier to move and reuse. The container belongs to the element responsible for providing the layout context; the component remains responsible for deciding how to use that context.
Style queries: conceptual introduction
Container queries are not limited to size in modern CSS. In supporting environments, style queries can react to selected computed custom-property values.
For example, a theme region might expose a value like this:
.theme-zone {
--theme: dark;
}
Descendants may then be able to query that style state, depending on browser support and the precise feature syntax being used. Treat advanced style queries and scroll-state queries as progressive enhancement, and verify current compatibility before relying on them in production. The concept is useful, but support details are part of the implementation decision.
Container query versus media query decision table
Use media queries when the decision belongs to the page environment:
- global page layout changes based on viewport or environment;
- user preferences such as reduced motion or color scheme;
- input capabilities such as hover or pointer;
- print styles.
Use container queries when the decision belongs to a reusable component:
- reusable cards or widgets that appear at different widths;
- component-local layout changes;
- typography or spacing bounded by component size.
Many real interfaces need both. A media query can respond to reduced motion while a container query changes a card's internal arrangement. They answer different questions, so one is not a universal replacement for the other.
Progressive enhancement
The component needs a usable baseline before any container-query enhancement is applied:
.card {
display: grid;
gap: 1rem;
}
Then add the container behavior where the support baseline requires it:
@supports (container-type: inline-size) {
.card-region {
container-type: inline-size;
}
@container (width >= 30rem) {
.card {
grid-template-columns: 8rem 1fr;
}
}
}
The @supports wrapper is optional when your browser-support baseline already guarantees the feature. The key requirement is that unsupported browsers still receive a coherent stacked card rather than an unusable component.
Deep dive: container selection and containment
A container query evaluates against an eligible ancestor that establishes the relevant query container. It does not inspect arbitrary ancestors:
.product-region {
container-name: product-region;
container-type: inline-size;
}
@container product-region (width >= 34rem) {
.product-card {
grid-template-columns: 10rem 1fr;
}
}
Naming the container makes the intended relationship explicit and avoids accidentally matching a nearer, unrelated container.
container-type: inline-size establishes the containment needed for inline-size queries. container-type: size is stronger: it can affect both axes. Do not choose it by default when the component only needs to respond to inline size, because containment can affect layout behavior beyond the query you intended to write.
Nested containers
Nested layouts can expose separate responsibilities:
.page-shell {
container: page / inline-size;
}
.sidebar-widget {
container: widget / inline-size;
}
A component can then target the container representing its own layout context:
@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. The page shell may be wide while the widget remains constrained inside a sidebar, and the widget's own container is the boundary that describes its actual choice.
Container query units: dimensions and bounds
The container-relative units can be grouped by the dimension they reference:
cqi— 1% of query container inline size;cqb— 1% of query container block size;cqw/cqh— width/height-based variants;cqmin/cqmax.
For example:
.card-title {
font-size: clamp(1.1rem, 4cqi, 1.8rem);
}
The bounds are still necessary. Proportional scaling alone can make text unreadable in an unusually small container or oversized in an unusually large one.
Style queries
In browsers that support them, container style queries can respond to custom-property values:
.card {
--density: compact;
}
@container style(--density: compact) {
.card__body {
padding: 0.75rem;
}
}
Use this as progressive enhancement. Check the exact support needed by the browsers you target; size queries remain the more common baseline.
Container queries versus component state
A container query answers a layout or environment question. It must not become a substitute for application state.
For example, this tries to infer business meaning from physical size:
/* do not infer business state from size */
@container (width < 20rem) {
.order { /* pretend order is "compact" business state */ }
}
If an order is selected, disabled, or in an error state, express that state with semantic HTML, attributes, or classes. Use container queries for presentation decisions caused by available space. A small order panel is not necessarily a compact business state, and the two concerns should remain independently testable.
Debugging container queries
When a query does not fire, debug from the container outward:
- inspect which ancestor establishes a container;
- check its
container-type; - confirm that the query uses the intended axis;
- confirm that 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 failure is not a malformed condition. It is asking the right question of the wrong container. DevTools' computed styles and layout information should help you identify the selected ancestor and its measured size before you change the breakpoint.
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, while container queries respond to the space available to a component. That makes reusable components less dependent on where they happen to be placed. Use each query type for the boundary it actually describes, and keep the component usable when the enhancement is unavailable.
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/
