The shift: from viewport to container
Viewport queries answer the wrong question for components: a card in a narrow sidebar and the same card in a wide content column face different viewports but identical containers. Container queries let the component respond to its own width — the sidebar card stacks its media above its text; the content-column card puts them side by side, from the same CSS, no duplication.
The mental model flips from 'how big is the screen?' to 'how much room does this component have?' — which is the question components can actually answer about themselves.
.card-grid {
container-type: inline-size; /* size queries off this container */
container-name: card; /* optional, for specificity */
}
.card { display: grid; gap: 1rem; }
.card__media { aspect-ratio: 4 / 3; }
@container card (width > 480px) {
.card { grid-template-columns: 240px 1fr; }
.card__media { aspect-ratio: auto; height: 100%; }
}Recipe 1 — the media object that knows its width
The classic: an avatar-plus-text row. Narrow (in a drawer), stack avatar above text. Wide, avatar left. One component, both layouts, and the breakpoint is the container's own width — it behaves identically in a sidebar at 1400px viewport and a phone at 700px, because the phone is the wide context for that drawer.
- Recipe 2 — the stats band: five stats in a full-width band become three-plus-two at container 700px, then two-plus-two-plus-one at 480px. The band queries its container, so embedding the band in a page section or a dashboard card both work without a second stylesheet.
- Recipe 3 — the pricing card: price tables flip from rows to stacked tiers by container width; the same card component serves the marketing page (wide container) and an embedded comparison widget (narrow container).
- Recipe 4 — the hero: hero components with container queries can live on the homepage (full width, split layout) and on a campaign page (contained, stacked) without variants or props-for-layout.
container-type: inline-size makes the element a size container — but it also makes the element's size depend on its contents only in the block axis, and it changes how percentage heights resolve. The classic breakage: an img with height: 100% inside a container query stops resolving. Query the wrapper, not the element whose height you are sizing.
When viewport queries still win
Page chrome — navs, sidebars, global layout — is genuinely viewport-sized and should stay on viewport queries. The rule of thumb: container queries for anything reusable, viewport queries for anything architectural. And container query units (cqw, cqh) for sizing type and spacing inside components complete the story — fluid type that responds to the component, not the monitor.