For fifteen years, media queries were the only responsive tool we had. Resize the viewport, trigger a breakpoint, restyle the layout. Simple, powerful, and — once you start building reusable components — deeply wrong.
The problem isn't media queries themselves. The problem is that a reusable component doesn't know where it will be placed. It only knows the viewport width — which is a poor proxy for "how much space do I have right now?"
The limitation media queries never solved
Imagine a card component. In a wide main content area, you want it to display with an image on the left and text on the right. In a sidebar or a three-column grid, you want the image on top and text below — compact. Same component, different contexts.
With media queries, you can't express this from inside the component. The breakpoint fires based on the window, not the card's container. If someone drops your card into a sidebar on a wide desktop viewport, it looks wrong — the viewport says "wide," but the component has 220px.
The traditional hacks: CSS Grid magic numbers, JavaScript resize observers, duplicated component variants with a compact prop. All of them are workarounds for the same missing primitive: a way for a component to ask its parent how much space it has.
Container queries: the mental model
Container queries let a component respond to the size of its parent container rather than the viewport.
/* Step 1: declare a containment context on the parent */
.card-wrapper {
container-type: inline-size;
}
/* Step 2: query the container's width from inside the component */
.card {
display: grid;
grid-template-rows: auto 1fr; /* image on top: the compact default */
}
@container (min-width: 500px) {
.card {
grid-template-columns: 200px 1fr; /* side-by-side when container is wide */
grid-template-rows: unset;
}
}
The card now reads: "When my container is at least 500px wide, go side-by-side." Drop it into a narrow sidebar and it stays stacked. Put it in a wide content grid and it opens up. One component, both layouts, no JavaScript, no prop threads.
Browser support landed across the board — Chrome, Firefox, Safari, and Edge all ship container queries without flags or polyfills.
Named containers for complex layouts
When layout contexts nest, you can name your containers and query a specific ancestor:
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
.main {
container-type: inline-size;
container-name: main;
}
/* This card responds to the sidebar's width, not its immediate parent */
@container sidebar (min-width: 300px) {
.card-title {
font-size: 1.25rem;
}
}
/* And separately to the main area */
@container main (min-width: 700px) {
.card-title {
font-size: 1.5rem;
}
}
Without named containers, making a deeply nested element aware of a high-level layout region would require a resize observer, a React context, and a prop drilling session. With named containers, it's two CSS declarations.
What still belongs in media queries
Container queries don't replace media queries — they have different jobs.
Media queries are still right for:
- Global page layout: switching from one column to two, collapsing a nav bar
- Anything that genuinely responds to the viewport — a full-bleed hero, a fixed header
- Print styles and forced-color / prefers-reduced-motion rules
Container queries are right for:
- Reusable UI components: cards, tables, form fields, media objects
- Any component placed in multiple layout contexts
- Any situation where the right layout depends on available space, not window size
A rough heuristic: page-level layout decisions → media query. Everything below the layout skeleton → container query.
The hidden benefit: components finally work in isolation
Container queries also fix a subtle problem with component development. When you design a card in Storybook with media queries inside it, the preview lies — it shows the component at the viewport width of the story frame, not the width it'll have in your actual app. The preview can look fine while the production layout looks broken.
With container queries, the component genuinely responds to the space the story gives it. Resize the story panel and the card adapts. What you see in isolation is what you get in context, because "context" is now encoded in the CSS rather than inferred from the window.
The takeaway
Container queries arrived quietly and they fix something that was quietly broken the whole time. If you're writing viewport breakpoints inside reusable UI components, you're using the wrong primitive — and you can fix it with one declaration: container-type: inline-size on the parent.
The core shift is small: stop asking "how wide is the window?" and start asking "how wide is my container?" Once the question changes, the component designs itself.
Top comments (0)