Author: Cheng Yi Xu
Target Publication: Smashing Magazine
Date: May 5, 2026
Container queries shipped across all major browsers in early 2025. A year later, they have fundamentally changed how front-end teams build responsive interfaces. But the migration path has been bumpier than the spec authors hoped, and the patterns that work in production are not always the ones you see in conference demos.
This article is not a tutorial. It is a postmortem from real teams that shipped container queries to millions of users over the past twelve months. I will cover what broke, what surprised us, and the patterns we wished we had known on day one.
Media queries answer one question: how wide is the viewport? Container queries answer a different question: how wide is this element?
That distinction sounds trivial until you live with it. Media queries are global. They force every component on the page to respond to the same breakpoint. Container queries are local. A sidebar card, a main-content hero, and a modal dialog can each make independent layout decisions based on their own available space.
The mental shift is from page-level thinking to component-level thinking. And it is harder than it looks.
A developer at a large European e-commerce company told me: "We spent three months converting our product grid to container queries. The CSS was cleaner. But we had eleven bugs where components rendered differently on the same page because their containers had different inline sizes. That never happened with media queries."
This is the double-edged sword. Container queries give you precision, but precision means you can no longer assume that all instances of a component look the same just because the viewport size is the same.
As of May 2026, container query support is excellent across all modern browsers. Chrome, Edge, and Opera shipped full support in early 2024. Firefox followed in late 2024. Safari completed its implementation in March 2025 with Safari 18.
The baseline is now 2025, meaning every browser that matters supports container queries. If your analytics show less than 2% of users on older browsers, you can use container queries without a fallback.
That said, a few details tripped us up:
Container query length units (cqw, cqh, cqi, cqb) shipped slightly later than the query syntax itself. Safari 17 supported @container but not cqw units. If you use container-relative units, test on Safari 17 specifically, or polyfill with calc().
Style queries (@container style(--variant: featured)) are still Baseline 2025 but have a separate support story. Safari shipped them in Safari 18. Chrome shipped them in Chrome 120. If you are using style queries for theming or variant switching, check your Safari traffic before removing fallbacks.
Nested container queries work, but performance degrades beyond three levels of nesting. A container that queries its parent, which queries its parent, which queries its parent, triggers layout thrashing on lower-end Android devices. One news site discovered their article sidebar was causing 200ms layout shifts on a Moto G Power because the byline component was four containers deep.
Rule of thumb from production: if your container query chain is more than two levels deep, flatten it.
The most successful migrations followed this sequence:
Do not convert your entire CSS codebase at once. Start by wrapping existing media-query-based components in container query contexts.
/* Step 1: Create the containment context */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Step 2: Keep your existing media queries for now */
.card {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 600px) {
.card {
grid-template-columns: 1fr 1fr;
}
}
/* Step 3: Add container query overrides */
@container card (min-width: 400px) {
.card {
grid-template-columns: 1fr 1fr;
}
}Over time, you remove the @media blocks as you gain confidence. But shipping both side by side for a sprint or two costs almost nothing and catches edge cases you would miss in code review.
Unnamed container queries (@container (min-width: 400px)) seem convenient. In production, they are a maintenance hazard.
A container with no name queries the nearest ancestor that has a container-type. This means if someone adds a container-type: inline-size to a wrapper three levels up in the DOM, your query silently changes its target. Debugging this took one team two days.
Always name your containers:
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
@container sidebar (min-width: 300px) {
.nav-list {
display: flex;
}
}The most common bug: a component that works perfectly in a wide viewport breaks inside a narrow sidebar, even though the sidebar itself is wide enough.
Why? Because inline-size containers only respond to width. If your component's layout depends on height (e.g., a fixed-height image gallery, a card that collapses when too short), you need container-type: size — which blocks the element from sizing based on its content and can destroy your layout if you are not prepared.
Production pattern: almost nobody uses size. Instead, teams use inline-size and handle height-dependent cases with min-height and max-height constraints on the component itself.
.gallery {
container-type: inline-size;
container-name: gallery;
min-height: 300px;
max-height: 80vh;
}
@container gallery (min-width: 600px) {
.gallery-grid {
grid-template-columns: repeat(3, 1fr);
}
}The most common container query pattern in production. Instead of a fixed number of columns at each viewport breakpoint, cards adapt to their container.
.product-grid {
container-type: inline-size;
container-name: product-grid;
}
.product-grid-inner {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@container product-grid (min-width: 400px) {
.product-grid-inner {
grid-template-columns: repeat(2, 1fr);
}
}
@container product-grid (min-width: 700px) {
.product-grid-inner {
grid-template-columns: repeat(3, 1fr);
}
}
@container product-grid (min-width: 1000px) {
.product-grid-inner {
grid-template-columns: repeat(4, 1fr);
}
}The breakthrough: this same card grid works inside a full-width main content area, a 60%-width article column, or a 300px sidebar. No media query duplication. No calc hack. Just one component, defined once.
A SaaS dashboard team reported that container queries eliminated 73% of their media query rules. Their CSS bundle dropped from 42KB to 18KB (uncompressed) after the migration.
Forms are the hardest thing to make responsive. Labels, inputs, help text, and error messages each have their own ideal width. Before container queries, teams often used a single-column layout for mobile and a two-column layout for desktop, with nothing in between.
.form-field {
container-type: inline-size;
container-name: field;
}
.field-inner {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
@container field (min-width: 500px) {
.field-inner {
flex-direction: row;
align-items: flex-start;
gap: 1rem;
}
.field-label {
flex: 0 0 160px;
text-align: right;
padding-top: 0.5rem;
}
.field-input-wrapper {
flex: 1;
}
}The form field now rearranges itself based on its own width, not the page width. Place it in a modal (narrow) and it stacks. Place it in a full-page form (wide) and it goes side by side. Same CSS.
This is the pattern that made me a true believer. Traditional responsive typography uses viewport units or media queries. Both are global. With container queries, typography scales to the component's own width.
.article-body {
container-type: inline-size;
container-name: article-body;
}
.article-body h1 {
font-size: clamp(1.5rem, 5cqi, 2.5rem);
}
.article-body p {
font-size: clamp(1rem, 2cqi, 1.125rem);
}When .article-body is 800px wide (full column), the heading is 2.5rem and body text is 1.125rem. When the same .article-body appears in a 400px sidebar, the heading shrinks to 1.5rem and body text to 1rem. All from the same CSS. No breakpoints. No duplicated rules.
The cqi unit (container query inline) is worth memorizing. It is cqw for horizontal writing modes, but cqi respects writing direction. If your site supports RTL languages or vertical writing modes, cqi is the correct unit.
One caution: Safari 17 users see clamp(min, 0, max) because cqi evaluates to 0 in browsers that do not support container-relative units. The clamp() function then resolves to the minimum value. This is actually a graceful degradation — your text is readable, just at the smallest size. If you need a more intentional fallback, use feature detection:
.article-body h1 {
font-size: 1.5rem; /* fallback for Safari 17 and below */
}
@supports (font-size: 1cqi) {
.article-body h1 {
font-size: clamp(1.5rem, 5cqi, 2.5rem);
}
}This pattern emerged from dashboard and analytics products. A parent container defines a layout zone, and child components inherit that zone's constraints through nested container queries.
.dashboard {
container-type: inline-size;
container-name: dashboard;
}
@container dashboard (min-width: 900px) {
.dashboard-layout {
display: grid;
grid-template-columns: 250px 1fr 300px;
}
}
.chart-panel {
container-type: inline-size;
container-name: chart;
}
@container chart (min-width: 500px) {
.chart-legend {
display: flex;
gap: 1rem;
}
}The chart panel queries its own container, not the viewport. Whether the dashboard is 1400px or 950px, the chart panel only cares about how much space it personally gets. The layout is a negotiation between containers, each making independent decisions.
This pattern eliminated an entire category of bugs where adding a new sidebar pushed a widget below a layout threshold and broke its internal arrangement.
Not everything is solved.
Debugging container queries in DevTools is harder than media queries. Chrome's Elements panel shows which container queries match, but only for the selected element. There is no "container query overview" like there is for media queries. Firefox has a slightly better story with its container query badge in the inspector, but it is still one-at-a-time debugging. For pages with 50+ containers, this is painful.
Third-party embeds break containment. If your container wraps an iframe, an ad unit, or a dynamically loaded widget, the container's inline size is determined by the embed. But the embed might resize asynchronously. Result: a container query that fires, then fires again 300ms later when the embed loads, causing a visible layout shift. The fix is to give containers explicit min-width or aspect-ratio when they wrap lazy-loaded content.
CSS Grid and container queries interact unpredictably with autoplacement. A grid item assigned to an auto-sized column has an indefinite inline size until the grid layout completes, which means the container query cannot resolve until after layout, which means the query result might be stale by the time it is applied. This is a spec-level issue (CSS Containment Module Level 3) that the working group is actively discussing. For now, always give grid items an explicit starting size in their container query context:
.grid-item {
container-type: inline-size;
/* Always set at least one dimension */
min-width: 0;
}
@container (min-width: 300px) {
.grid-item-content {
display: grid;
grid-template-columns: 1fr 1fr;
}
}The min-width: 0 prevents the grid item from having an indefinite minimum size, which ensures the container query can resolve.
Server-side rendering does not know about container sizes. At SSR time, there is no layout. Container queries always resolve to false. This means components that depend on container queries for their initial render will show a fallback state until hydration. Teams using Next.js or Nuxt have adopted one of two workarounds: (1) precompute the container size when the initial viewport is known, or (2) add a CSS transition so the shift from fallback to hydrated layout is smoothed over.
.card {
transition: grid-template-columns 0.15s ease;
}A 150ms transition is imperceptible to users but hides the SSR-to-hydration jump.
Several teams shared performance data with me. The summary: container queries are not free, but they are cheaper than the JavaScript-based solutions they replace.
A media company that previously used ResizeObserver to calculate component widths and toggle CSS classes reported a 35% reduction in main-thread scripting time after switching to container queries. The layout work moved from JavaScript to the browser's native layout engine, which is parallelized and GPU-accelerated.
However, container queries do add layout cost. Each container-type declaration creates a containment context, which the browser must track. In a stress test with 1,000 container-typed elements on a single page, Chrome's layout time increased by approximately 12% compared to the same layout without container queries. This is unlikely to matter for most pages, but if you are building an infinite-scroll feed or a virtualized list, be selective about which elements get container-type.
One fintech team found that adding container-type: inline-size to every row in their data table (200+ rows) caused visible jank during scroll. They fixed it by applying container queries only to the table wrapper, not to individual rows:
/* Bad: 200+ containment contexts */
.data-table tr {
container-type: inline-size;
}
/* Good: one containment context */
.data-table-wrapper {
container-type: inline-size;
container-name: table;
}Style queries deserve their own section because they unlock patterns that were previously impossible without JavaScript.
Style queries check CSS custom property values, not dimensions:
@container style(--variant: featured) {
.card {
border-color: var(--color-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}A design system team I spoke with uses style queries for their component variant system. Their button component has --button-variant: primary | secondary | ghost | danger. Instead of chaining .btn-primary, .btn-secondary, etc., they set one custom property and let container queries handle the rest:
.btn-wrapper {
--button-variant: primary;
}
@container style(--button-variant: primary) {
.btn {
background: var(--color-brand);
color: white;
}
}
@container style(--button-variant: ghost) {
.btn {
background: transparent;
color: var(--color-brand);
border: 1px solid var(--color-brand);
}
}This decouples the variant selection (which happens in the parent) from the visual implementation (which happens in the component). It is essentially component-level theming, and it eliminates the combinatorial class explosion that plagues many design systems.
The limitation: style queries currently only support equality checks (style(--var: value)). There is no min- or max- for custom property values. CSS Working Group discussions are ongoing for range queries on registered custom properties.
If you support only modern browsers (Baseline 2025), the answer is yes. The benefits are real:
- Smaller CSS bundles. Teams report 40-70% fewer media query rules after migration.
- Truly reusable components. A component works the same way regardless of where it is placed in the DOM. This is the holy grail of component-based design.
- Simpler responsive logic. No more
useMediaQueryhooks andResizeObserverboilerplate in your JavaScript. - Faster development. Adding a new page layout no longer means rewriting responsive rules for every component on that page.
If you still support Safari 17 or older Chrome versions (more than 5% of your traffic), you can use progressive enhancement. Ship container queries with @supports guards and keep your media queries as fallbacks. Your CSS bundle will be larger during the transition, but your users will see the same layout regardless.
-
Always name your containers. Unnamed queries create silent coupling between your CSS and the DOM structure. Naming is the difference between three hours of debugging and zero.
-
Use
inline-sizeunless you have a specific reason to usesize. The latter creates a sizing deadlock if you are not careful, and most responsive patterns only need width awareness. -
Container query length units (
cqi,cqw,cqb,cqh) are the killer feature for typography. Pair them withclamp()for fluid type that scales to the component, not the viewport. -
Add a CSS transition to container-queried elements so SSR-to-hydration layout shifts are invisible. 150ms is all you need.
-
Do not put
container-typeon every element. Be deliberate. Each containment context has a layout cost. Wrap logical layout zones, not individual list items or table cells. -
Style queries are ready for variant-based design systems. If you have a component that comes in four or five visual variants, style queries will simplify your CSS more than you expect.
CSS Container Queries are not a replacement for media queries. They are a complement. Media queries still answer the question "what device is this?" while container queries answer "how much room do I have?" Both are useful. The art is knowing which to use when.
Over the next year, I expect to see container queries become the default responsive mechanism for component libraries, with media queries reserved for top-level page layout decisions. The CSS Working Group is also exploring container-relative viewport units and range queries for style queries, both of which will make container queries even more powerful.
If you have not started your migration, start with one component. Pick a card, a form field, or a media object. Convert it. Ship it. Watch your analytics for layout bugs for a week. Then do another. The incremental approach costs less than a rewrite and surfaces problems when they are easy to fix.
A year in, the verdict is clear: container queries are production-ready, and the teams that adopted them are not going back.
Author Bio: Cheng Yi Xu is a front-end developer and technical writer specializing in CSS architecture, design systems, and web performance. He writes about real-world production experiences with new web platform features.