CSSTidy CSS Formatter

Media Queries and Container Queries: When to Switch, and How to Migrate

CSSTidy — CSS Formatter & Minifier Guides · Updated 2026-10-12 · All guides

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

  1. container-type changes layout containment. inline-size implies contain: 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.
  2. A container cannot query itself. Style the children of the container, not the container. @container card (width > 500px) { .card { … } } where .card is the container does nothing — the query evaluates against the container, so put the query on .card > * or wrap it.
  3. container-name is not required, and omitting it is safer. Without a name, a query matches the nearest ancestor with the right container-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.
  4. 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.
  5. 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:

UnitRelative to
cqi / cqbcontainer's inline / block size
cqw / cqhcontainer's width / height (only valid in a container query context)
cqmin / cqmaxsmaller / 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:

  1. 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.
  2. 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-col is documentation.
  3. Move the media rule into a container rule, keeping the pre-existing default. If @media styles were the override, @container styles stay the override. If they were the base, invert: base becomes the compact form.
  4. 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.
  5. Convert vw sizing last, once the container boundary is settled — cqi values 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 @container rule 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.

Developer Sponsor / Partner
Copied to clipboard!