Building components that fit any context
When you write CSS for a component, the goal is often to make it adaptable enough to drop into a variety of containers without looking out of place. That gets harder when the component has to work both in wide grid layouts and in a narrow sidebar, as a product card might on a store page. Container query units and explicit container queries give you the tools to make those adjustments based on the space actually available, rather than the viewport alone.
Units as shorthand for browser-provided variables
Before setting up your own custom properties, remember that the browser provides several useful named units that can carry much of the sizing work:
1em: the current font size.1rem: the font size on the:root (html)element.1lh/1rlh: the current and root line heights.1vw: the viewport width.1vi: the viewport "inline" size (for English, the same asvw).1cqi: the inline size of the nearest "container" (defaulting to the viewport).
These are effectively variables with a built-in multiplication syntax. Where you might write calc(0.5 * var(--line-height)) for a custom property, the lh unit lets you write 0.5lh instead. The value of a unit expresses a relationship, not a fixed expectation: 1em may happen to equal 16px in one context, but that equivalence isn't stable.
For spacing between blocks of prose, lh-based units maintain a consistent vertical rhythm with no extra work. When you're spacing content inside cards or between grid tracks, however, you need to think about two things. Multiples of 1lh can still maintain page-level vertical rhythm, but cqi accounts for the actual amount of available space. A round() function can combine both by starting from container size and snapping upward to a multiple of quarter-lines:
html {
--gap: round(up, 2cqi, 0.25lh);
}
With a line-height of 20px, --gap becomes a multiple of 5px, but the specific multiple tracks with available space. Swap cqi for the viewport-relative vi unit and the same variable stays consistent across the entire page instead of per-container.
Fluid type that respects user preferences
The same pattern applies to fluid font sizes. A clamp() with a base of 1rem keeps the size close to the user's chosen font preference, while a small cqi addend lets it scale within a bounded range:
html {
--body-text: clamp(1rem, 0.875rem + 0.5cqi, 1.25rem);
}
With the range clamped between 1rem and 1.25rem, the cqi term controls how fast the font responds to space, and the rem offset keeps it anchored to the user's setting. Keep the rem value near 1 and the cqi value low; the closer you get to 100cqi or the further you drop below 1rem, the less influence the user's font preference has on the result. For a card grid, item titles can use a slightly larger, container-based size, while list-level headings might use vi units so they match across all placements, regardless of the immediate context.
Measuring the real container
By default, 1cqi equals 1svi (the small viewport inline size), because the viewport acts as the initial container. To make cqi useful beyond that, you must declare additional containers. Consider a layout with a product-list and a shopping-cart; exposing their inline-size makes them the measuring context for everything inside:
product-list,
shopping-cart {
container-type: inline-size;
}
One limitation: container query units cannot measure the element they're applied to. Setting the shopping-cart's own width in cqi would create a circular dependency, so it resolves against the next ancestor that is a defined container. Similarly, when grid cells shrink based on the number of columns that fit, those cells aren't themselves registered as containers. Each product-detail component therefore wraps its card markup in a nested <article>, with the outer product-detail element serving only as the measurement container. The card inside gets its spacing and type recalculated for the cell's real available width.
Breakpoints at the container, not the viewport
Units handle continuous adjustments well, but crossing a threshold often calls for a more pronounced change. A viewport media query is the right tool when the shift is tied to overall screen size, such as moving the shopping-cart from above the main list into a sidebar once there's enough horizontal space:
main {
display: grid;
grid-template:
'controls' auto
'cart' 1fr
'list' auto
/ minmax(min-content, 1fr);
@media (width > 30em) {
grid-template:
'controls controls' auto
'list cart' 1fr
/ 2fr 1fr
;
}
}
The problem is that the product components live inside the shopping-cart. When the viewport crosses that sidebar breakpoint, the shopping-cart suddenly gets narrower, and the products lose their space. A container query uses nearly identical syntax but evaluates against the container size instead:
article {
display: grid;
grid-template:
'image' auto 'title' auto
'summary' auto
'button' auto / auto
;
@container (inline-size > 40ch) {
--image-ratio: 1;
grid-template:
'image title' auto
'image summary' 1fr
'image button' auto
/ minmax(50px, min(20%, 500px)) 1fr
;
}
@container (inline-size > 50ch) {
grid-template:
'image title title' auto
'image summary button' 1fr
/ minmax(50px, min(20%, 500px)) 1fr fit-content(20%);
}
}
With container queries, product-list and product-detail only need to know how much space they have, not which component took space away to create that situation. That's what keeps components reusable across unrelated layouts.
Animating grid shifts and card visibility
Frequent layout changes need smoothing. Grid layouts can animate column and row sizes and the gaps between them, but there are two constraints: initial and end states must have the same track count, and animated tracks must use comparable length units. Rather than swapping between a one-column and two-column grid, keep the sidebar column at 0 width with a 0 column-gap when hidden, then transition to calc(15em + 1cqi) and var(--gap) when open. Both start and end are normal length values, so the animation works from a literal 0:
main {
transition: grid-template 250ms, gap 250ms;
}
Cart items and product details animate in and out with two features that are not yet Baseline but degrade gracefully. The first, interpolate-size: allow-keywords, unlocks transitions on dimensions from 0 to auto. Because the property inherits, declaring it once on the <html> element covers every child. It's currently limited to Chrome-based browsers, but unsupported engines simply skip the animation. The second feature is animating discrete properties like display, where no intermediate state exists between grid and none. Adding allow-discrete to transition-behavior causes the flip to fire at the start or end of the transition duration. The transition-behavior property itself has wide support, but toggling display with it still lacks Firefox support—again, a progressive enhancement with a working static fallback.
Choosing a context for your styles
Container queries and container units often stand in for media queries and viewport units, and that substitution is worthwhile when it clarifies a design's intention. But neither approach is universally superior. Media queries and viewport units remain the right tool when you need sizing and spacing that stays consistent in relation to the entire viewport. Container queries earn their place when you want those properties to respond to a component's immediate surroundings.
CSS is most effective when the relationships in code align with the goals of the design. Use container queries and units for text and spacing tied to local context. Rely on media queries and viewport units for global, viewport-relative consistency. In practice, most sites are best served by a combination of both. In practice, most sites will likely use a mix of both methods, choosing based on what each style rule needs to express. In practice, most sites will likely find a balance between them, applying each where it most clearly reflects the design's purpose.



