Skip to content

Container queries killed my breakpoints

Media queries ask how big the window is. That was never the question. Components care about the space they are given, and now they can ask.

2 min read
Contents

Here is a bug I have shipped more than once. A product card looks perfect in a three-column grid. Somebody drops the same card into a narrow sidebar. The window is still 1440px wide, so the media query still says “desktop”, and the card renders its wide layout into a 280px column and falls apart.

The card was asking the wrong question. It asked how big the window is. What it needed to know was how much room it has.

The two lines that fix it

.card-slot {
  container-type: inline-size;
}

.card {
  display: grid;
  gap: 1rem;
}

@container (min-width: 28rem) {
  .card {
    grid-template-columns: 10rem 1fr;
    align-items: start;
  }
}

The parent declares itself a container. The child queries that container instead of the viewport. Drop the card in a sidebar and it goes vertical. Drop it in a wide grid and it goes horizontal. Same component, no props, no variant="compact", no JavaScript measuring anything.

The first time this clicked for me I went and deleted a ResizeObserver that had been in a client codebase for two years.

Name your containers

Unnamed containers resolve to the nearest ancestor with container-type, which is fine until you have nested ones and something queries the wrong box.

.page { container: layout / inline-size; }
.card-slot { container: card / inline-size; }

@container card (min-width: 28rem) { /* … */ }
@container layout (min-width: 60rem) { /* … */ }

The container shorthand is name / type. Name everything from the start; it costs one word and saves an afternoon.

Container query units

Alongside the query itself you get units relative to the container: cqw, cqh, cqi, cqb, cqmin, cqmax.

.card h3 {
  font-size: clamp(1rem, 4cqi + 0.5rem, 1.75rem);
}

That heading scales with the space the card is in, not with the window. A card in a sidebar gets a sidebar-sized heading, automatically, with no breakpoints at all. This is the thing that makes truly reusable components feel effortless, and it does not have an equivalent in the media query world.

Style queries, for the variant problem

Container style queries let a component react to a custom property set anywhere up the tree:

.panel { --tone: neutral; }
.panel.danger { --tone: danger; }

@container style(--tone: danger) {
  .panel-heading { color: var(--red); }
  .panel-icon { rotate: 180deg; }
}

This is how you theme a subtree without threading a prop through four components or writing .panel.danger .panel-heading chains. Support for style queries lagged behind size queries, so check before you rely on it — but the size queries above are Baseline and safe.

What I still use media queries for

Container queries did not replace media queries; they took over the job media queries were being misused for.

Media queries are for the environment: prefers-reduced-motion, prefers-color-scheme, print, pointer: coarse, and the handful of genuinely page-level layout shifts — when the sidebar appears, when the nav collapses into a menu.

Container queries are for everything inside those regions. In a recent project the split came out at four media queries for the page shell and about twenty container queries across components, replacing something like sixty media queries of the old kind.

The real change is organisational rather than technical. With media queries, responsive behaviour lives in a breakpoint file that nobody owns and everybody edits. With container queries it lives in the component, next to the rest of that component’s CSS, and it is correct wherever the component ends up.

Which means the next time somebody drops your card into a narrow sidebar, nothing happens. That is the whole point.

Share