30/5/2026 · 15 min read
For over a decade, media queries were the only responsive layout tool we had. The problem was never that they did not work — it was that they forced every component in a codebase to know something it should never have needed to know: the width of the viewport. A card component, a navigation widget, a sidebar region — all of them ended up with breakpoints baked in that assumed a specific page layout. Change the layout and the component breaks. Move the component into a sidebar and it still behaves as if it owns the full page width. That coupling between a component and its hosting context was the real cost of the media-query era, and it compounded with every design iteration.
Container queries sever that dependency completely. A component no longer needs to know where it lives on the page. It only needs to know the size of its own container — and the browser provides that. The result is components that are genuinely reusable across layouts: the same card component renders correctly whether it sits in a three-column grid, a single sidebar, or a full-width hero region. No extra modifier classes, no JavaScript, no duplicated stylesheet rules for every new context. This is the abstraction level that responsive design always should have been built on, and in 2026 it is baseline browser support with no caveats.
The core issue with media queries is that they query the wrong thing. A media query asks "how wide is the browser window?" when the question a component actually needs answered is "how much space do I have right now?" Those two questions have the same answer only when a component spans the full viewport — which is true for maybe one component per page. Every other component lives inside a layout that has already consumed some of that viewport width, and the media query has no awareness of that.
The practical consequence is a class of bugs that are deeply familiar to anyone who has maintained a large CSS codebase. A card grid component is built and tested in a full-width context. It breaks into three columns at 900px and two columns at 600px. It looks correct. Then a designer decides that the same card component should also appear in a 320px sidebar. The breakpoints are all wrong. You add a modifier class. The sidebar layout changes. The modifier class is now wrong too. You add another. Over time you accumulate a brittle network of context-specific overrides, and the component stops being reusable in any meaningful sense.
Container queries solve this at the right abstraction level. The component only cares about its container's dimensions — the space actually allocated to it by its parent layout. The parent layout does not need to know anything about the component's internal breakpoints. The coupling is gone in both directions.
The syntax is minimal and deliberate. To make an element a container, you set container-type on it. The value inline-size tells the browser to track the container's inline axis (horizontal in a left-to-right writing mode). Then inside your component's stylesheet you write @container rules that behave exactly like @media rules, except they resolve against the container rather than the viewport.
The parent wrapper gets the container declaration. This is the element that "owns" the space your component lives in — a grid cell, a sidebar column, a modal body, whatever the layout context happens to be:
.card-wrapper { container-type: inline-size; }
That single property is the entire opt-in. The element is now a named containment context. Any descendant can now write container queries against it.
Inside your component's CSS, you replace your media query with an @container block. The syntax mirrors media queries exactly:
@container (min-width: 400px) { .card { flex-direction: row; } }
When the container is narrower than 400px the card stacks vertically. When it is wider than 400px the card lays out horizontally. This logic lives entirely inside the card component's stylesheet with zero knowledge of where in the page the card is rendered.
When you have nested layouts and need to target a specific ancestor rather than the nearest container, you assign a name using the container-name property:
.sidebar { container-type: inline-size; container-name: sidebar; }
Then in your query you address it by name: @container sidebar (min-width: 280px) { ... }. This is particularly useful in component libraries where the same component might be used inside several different containers at different nesting depths.
Before container queries, a typical card component carried viewport breakpoints: @media (max-width: 768px) { .card { flex-direction: column; } }. That rule fires based on the browser window, not the card's actual context. If the card sits in a two-column grid, it might already need to stack at 768px of grid-cell width — but the viewport is still 1200px wide, so the media query never fires. The result is a card that overflows its container.
After container queries, the card's CSS becomes honest: @container (max-width: 360px) { .card { flex-direction: column; } }. The card stacks when its own container is narrow. It does not matter whether that container is narrow because the page is on a mobile phone or because it lives in a sidebar on a desktop. The component adapts correctly in both cases with a single rule.
Size-based container queries answer "how wide is my container?" Style queries answer a different question entirely: "what is the computed value of a CSS custom property on my container?" This opens up a form of contextual theming that was previously only possible with JavaScript or deeply nested class hierarchies.
The syntax uses @container style() with a custom property check. For example, suppose your design system defines a compact variant for dense UI contexts. Instead of passing variant props through every component layer, you set a custom property on the container:
.dense-panel { --variant: compact; }
Then inside any component that lives in that panel, you query for it: @container style(--variant: compact) { .card { padding: 8px; font-size: 0.875rem; } }
The card adjusts its own density based on what the parent panel declared — no prop drilling, no extra class on the card itself, no JavaScript. The parent sets intent, the child reads it. This pattern maps naturally to design token contexts: a region of the UI can declare itself as a "promotional" or "editorial" or "system" context, and any component inside it can adapt accordingly without needing to know anything about the specific custom property values.
The most powerful application is theme inheritance. Light and dark mode are the obvious example, but style queries allow much more granular contextual theming. A dashboard panel can declare --surface: elevated and every card, input, and divider inside it can respond to that declaration with appropriate shadow, border, and background adjustments — all in CSS, all without touching markup or JavaScript. In practice this means your component library can be genuinely context-aware at the CSS level for the first time.
Browser support for style queries is still catching up to size queries as of mid-2026, but the spec is stable and the major engines have shipped it. For production use, check caniuse and use a feature query fallback: @supports (container-type: inline-size) { ... }.
The theoretical elegance of container queries only matters if they solve actual interface problems. Here are the patterns that container queries handle better than anything that came before them.
Container queries are a genuine improvement over media queries for component-level responsiveness, but they have real limitations that are worth understanding honestly before you commit to them as a complete solution.
Setting container-type: size to enable block-axis (height) queries creates layout containment on both axes, which means the container can no longer use intrinsic sizing for its block dimension. In practice this means you need to give the container an explicit height — which is often exactly the thing you do not know in a fluid layout. Inline-size containment has no equivalent side effect, which is why container-type: inline-size is the safe default and height-based queries remain niche in 2026.
This is the most important conceptual constraint. The element that declares container-type cannot use an @container rule to style itself. Container queries only apply to descendants of the container. If you try to style the container element itself based on its own size, you need a different approach — usually a ResizeObserver in JavaScript or a wrapper element. This is not a browser bug; it is a deliberate design decision to prevent circular layout dependencies.
An @container query inside a cross-origin iframe resolves against the iframe's own document, not the container in the host page that holds the iframe. If you are building embeddable widgets served from a different origin, container queries work within the widget's own stylesheet but they cannot respond to the host page's layout context. You still need postMessage-based communication or CSS custom properties passed via the iframe's style attribute for that scenario.
Container queries let a child respond to its container's size or custom property values. They do not let you select the container itself based on its children's state. Selecting parent elements based on child state is a separate problem that the CSS :has() selector addresses, and the two features complement each other well — but they are distinct tools for distinct problems.
Adopting container queries in a codebase that already uses media queries does not require a rewrite. The two features coexist cleanly, and the migration can be genuinely incremental — one component at a time, starting with the ones that move between layout contexts most often.
The best first candidates for migration are the components that already have the most context-specific media query overrides. If you have a card component that has been patched with special-case breakpoints five times because it kept breaking in new layouts, that is the component that will benefit most immediately from a container query rewrite. Identify the five to ten most over-patched components in your codebase and start there.
A low-risk first step is to add container-type: inline-size to the wrapper elements of your most common layout regions — the main content area, the sidebar, the modal body, the card grid — without changing any of the existing media queries. This is a non-breaking change. It makes those elements container contexts so you can start writing container queries against them, but it does not alter existing media query behavior at all.
Once a component's parent wrappers are established as containers, you can begin converting the component's media queries to container queries. Delete the viewport breakpoint, write the equivalent @container rule, and test the component in all the contexts it appears in. Because the container query responds to actual available space rather than viewport size, you will often find that a single container query replaces two or three media query overrides that existed to handle different layout contexts.
Media queries still belong in your codebase — they are the right tool for layout-level decisions that genuinely depend on the viewport. Page-level grid definitions, the switch between mobile and desktop navigation structures, top-level spacing adjustments: these are all still appropriately expressed as media queries. The rule of thumb is simple: if the style change responds to how the page is structured, use a media query. If the style change responds to how much space a component has, use a container query.
Container queries have baseline support across all major browsers in 2026, but if you maintain code that runs on older clients or within third-party embedding contexts with unpredictable browser targets, wrap your container query blocks in a @supports (container-type: inline-size) { ... } check. Browsers that do not support container queries will fall through to the baseline media query styles, maintaining a functional — if less sophisticated — experience. Remove the feature query wrap once your browser support matrix makes it redundant.
The most important non-CSS step in the migration is updating how your team thinks about and documents components. A component migrated to container queries has a different contract than one using media queries: it no longer has specific viewport breakpoints, only container breakpoints. Design system documentation, Storybook stories, and component API docs should all reflect the container context each component expects to live in. Teams that skip this step end up re-learning the same lessons when the next developer tries to embed the component in a new layout and wonders why it does not have the expected breakpoints.
Media queries were never a bad idea — they were a revolutionary one that got overloaded. We reached for them to solve component-level layout problems because nothing better existed. Container queries are that better thing. They let us write component styles that are honest about what they depend on, composable in any layout, and maintainable across design iterations without accumulating context-specific overrides.
The migration path is gradual by design. You do not have to rewrite anything. Start with the components that cause the most pain, establish container contexts in the wrapper elements that already exist, and convert one component's media queries at a time. Within a few weeks of that approach you will have a set of genuinely portable components and a much clearer mental model of which CSS tool belongs at which level of the UI hierarchy.
In 2026 there is no reason to ship a new component with viewport-based breakpoints unless that component genuinely represents a full-viewport-width layout element. Everything else — every card, every nav item, every content region, every embeddable widget — deserves a container query. The browser support is there. The syntax is learnable in an afternoon. The payoff compounds with every design system refactor and every new layout context you add. Use it.