Media Queries and Container Queries: When to Switch, and How to Migrate
Media queries ask the viewport a question the component did not ask: "how wide is the window?" Container queries ask the nearest sized ancestor: "how wide am I?" That distinction is what makes a component reusable in a sidebar and a full-width page without a --wide class per placement.
Start with size, then style
/* size container */
.card-area { container-type: inline-size; container-name: card; }
@container card (width >= 480px) {
.card { grid-template-columns: 120px 1fr; }
}
/* style container — no size needed */
.theme { container-type: inline-size; container-name: theme; }
@container theme style(--density: compact) {
.card { padding: .5rem; }
}
The style() form is what replaces most data-theme / .dark selector duplication: a token flips, descendants respond, and you never write .dark .card, [data-dark] .card again. It pairs directly with the token layers in custom properties.
The rules that actually bite
container-typechanges layout containment.inline-sizeimpliescontain: inline-size, which means the element's own width no longer depends on its children. A container that used to grow to fit a long<pre>now stays at its constrained width and the content overflows. This is the single most common regression when someone adds a container query to an existing component.- A container cannot query itself. Style the children of the container, not the container.
@container card (width > 500px) { .card { … } }where.cardis the container does nothing — the query evaluates against the container, so put the query on.card > *or wrap it. container-nameis not required, and omitting it is safer. Without a name, a query matches the nearest ancestor with the rightcontainer-type; with a name, it matches any element with that name anywhere up the tree — including one 8 levels up that you did not know about.- Height queries do not exist for size containers.
container-type: size(both axes) requires the container's block size to not depend on its children, which almost never holds in flow layout. Design for inline-size and let height follow. - Fallbacks are the same problem as any new feature. Wrap the enhancement:
.card { grid-template-columns: 1fr; } /* always-correct default */
@container card (width >= 480px) {
.card { grid-template-columns: 120px 1fr; }
}
Old engines ignore the block and keep the default. Never write the container-query version as the only rule — a grid-template-columns: 120px 1fr inside @container with no default is a one-column-collapse bug on any engine without support.
Units, and why vw is a bug
vw is viewport-relative forever: a modal at 40vw is 300 px inside a 750 px sidebar. Container units are relative to the queried container:
| Unit | Relative to |
|---|---|
cqi / cqb | container's inline / block size |
cqw / cqh | container's width / height (only valid in a container query context) |
cqmin / cqmax | smaller / larger of the two |
.card__media { width: 25cqi; } /* a quarter of the card, whatever the card is in */
cqi is the one worth adopting: it makes width: 33vw-style layout arithmetic disappear.
Migrating a component off viewport breakpoints
The mechanical path, one component at a time:
- Identify components with a breakpoint that is really about content. The tell:
@media (min-width: 900px) { .card { … } }where the card has nothing to do with the window at 900 px. - Put the container on the slot, not the component. The grid cell / sidebar / article column is the thing with the width you care about. Naming it is a refactor in itself:
container-name: main-colis documentation. - Move the media rule into a container rule, keeping the pre-existing default. If
@mediastyles were the override,@containerstyles stay the override. If they were the base, invert: base becomes the compact form. - Delete the old rule only after checking every usage. A media query is global; a container query is scoped. The component that looked like a single user may have been relying on the global query to style three other places.
- Convert
vwsizing last, once the container boundary is settled —cqivalues are meaningless while you are still unsure which element is the container.
What media queries are still for
Container queries did not obsolete them. Keep viewport queries for:
- Page-level layout — one column on a phone, two on a desktop. The page is the viewport.
- Root typography and spacing scale —
:root { font-size: clamp(15px, .9vw + 12px, 18px) }. - Preferences:
prefers-reduced-motion,prefers-color-scheme,prefers-contrast,display-mode: standalone. These have no container equivalent. - Anything that must apply before a container exists — a
@containerrule cannot style the container that scopes it (rule 2).
The rule of thumb: containers for components, media for the document and for user preferences. If you cannot say which element's width drives the decision, it is still a media query.
Related: grid vs flexbox for the layouts container queries usually resize, and DevTools debugging — the Layout pane shows which element is the container for a given query, which is the question that otherwise costs twenty minutes.