Container Queries in Production: When @container Finally Beats Media Queries

Media queries were designed when "responsive design" meant "resize the browser window and see what breaks." That was fine in 2012. In 2026, you’re building reusable components that live in sidebars, modals, dashboards, and full-width hero sections — all on the same page. A media query has no idea about any of that. It only knows how wide the viewport is.

That’s the core problem. And @container solves it properly.

Container queries have been in all major browsers since late 2023, and browser support is now effectively universal. There’s no polyfill needed, no build-step magic, no JavaScript. Just CSS that’s actually aware of where a component is rendered. If you’re still avoiding them in production, this article will change that.


The Fundamental Problem with Media Queries for Components

Here’s a concrete scenario. You build a product card component. You write media queries so it switches from a vertical to horizontal layout at 600px viewport width. It looks great in Storybook, great on the product listing page.

Then a designer drops the same card into a two-column sidebar. The viewport is 1200px wide — wide enough that your media query fires and forces the horizontal layout — but the column is only 300px wide. The card is now broken, and the media query has no idea.

You patch it. Then the card shows up in a modal. You patch it again. Then there’s a mobile-first dashboard with a collapsible nav that changes the available width dynamically. You’re now maintaining 14 variations of the same component, all fighting with each other through global media breakpoints.

This is what media queries actually are: global state attached to the viewport, not the component. They were never designed to solve component-level layout. We just used them that way because there was nothing better.


What @container Actually Does

A container query lets an element observe the size of its containing block — an ancestor you designate — and apply styles based on that, not the viewport.

You define a containment context on a parent:

/* Make this element a container */
.card-wrapper {
  container-type: inline-size;
  container-name: card; /* optional but recommended */
}

Then query it from a child:

/* Fires when .card-wrapper is wider than 400px */
@container card (min-width: 400px) {
  .product-card {
    flex-direction: row;
    gap: 1.5rem;
  }
}

Now that product card responds to its own context. Drop it in a 300px sidebar, 900px main column, or a 200px widget — it always does the right thing. No patches, no !important wars.


Setting Up Containment Correctly

The most common mistake is applying container-type to the wrong element. The container must be an ancestor of the elements you’re querying — you can’t query the element against itself.

/* WRONG — card is querying itself */
.product-card {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .product-card { /* never fires as you expect */ }
}
/* CORRECT — wrapper is the container, card is the query target */
.card-wrapper {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .product-card {
    flex-direction: row;
  }
}

container-type values

  • inline-size — tracks the inline axis (width in horizontal writing modes). This is what you want 95% of the time.
  • size — tracks both axes. Required if you need height-based queries. Has a higher layout cost; only use it when you actually need vertical queries.
  • normal — establishes a named container but doesn’t track size. Useful for style queries (more on that below).

Always prefer inline-size over size. Tracking both axes forces the browser to resolve layout in two passes, which adds cost, especially in scroll-heavy UIs.


A Real Production Pattern: The Adaptive Card

Here’s a full component pattern that works across every context without media query patches.

<!-- The wrapper is the container -->
<div class="card-wrapper">
  <article class="product-card">
    <div class="card-image">
      <img src="product.jpg" alt="Product name" />
    </div>
    <div class="card-body">
      <h2 class="card-title">Product Name</h2>
      <p class="card-description">Short description here.</p>
      <div class="card-meta">
        <span class="card-price">$49.99</span>
        <button class="card-cta">Add to cart</button>
      </div>
    </div>
  </article>
</div>
/* ---- Containment setup ---- */
.card-wrapper {
  container-type: inline-size;
  container-name: product-card;
  /* Wrapper is display-agnostic — grid, flex, or block all work */
}

/* ---- Base (narrow) styles ---- */
.product-card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  overflow: hidden;
}

.card-image img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
}

.card-body {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.card-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: auto;
}

/* ---- Medium container: side-by-side layout ---- */
@container product-card (min-width: 400px) {
  .product-card {
    flex-direction: row;
  }

  .card-image {
    flex: 0 0 40%;
  }

  .card-image img {
    height: 100%;
    aspect-ratio: auto;
  }

  .card-body {
    flex: 1;
    padding: 1.25rem;
  }
}

/* ---- Wide container: show more detail ---- */
@container product-card (min-width: 600px) {
  .card-description {
    /* In narrow contexts we hide it to save space */
    display: block;
  }

  .card-cta {
    min-width: 120px;
  }
}

Drop this card anywhere — a grid, a list, a sidebar widget, a modal — and it handles itself. Zero context-specific overrides.


Naming Containers: When It Matters

Without a name, @container queries walk up the DOM and target the nearest ancestor with a containment context. That’s usually fine, but it breaks down fast when containers are nested.

.dashboard {
  container-type: inline-size;
  /* no name */
}

.widget {
  container-type: inline-size;
  /* no name */
}

@container (min-width: 500px) {
  /* This targets .widget, the nearest context — probably what you want */
  .widget-content { ... }
}

Once you have widgets inside widgets, or components inside layout components, the "nearest ancestor" rule becomes hard to reason about. Name your containers:

.dashboard {
  container-type: inline-size;
  container-name: dashboard;
}

.widget {
  container-type: inline-size;
  container-name: widget;
}

@container dashboard (min-width: 900px) {
  /* Only fires based on dashboard width */
  .sidebar { display: block; }
}

@container widget (min-width: 300px) {
  /* Only fires based on widget width */
  .widget-chart { height: 200px; }
}

The shorthand property: container: widget / inline-size; — name first, then type. Use it.


Style Queries: The Hidden Power Feature

Size queries get all the attention, but style queries are what make container queries genuinely composable. They let you query the computed value of a custom property on the container.

Imagine a card component that needs to know if it’s inside a "featured" section:

/* Parent sets a flag */
.featured-section {
  --featured: true;
  container-type: normal; /* no size tracking needed */
  container-name: section;
}

/* Child queries the flag */
@container section style(--featured: true) {
  .product-card {
    border-color: var(--accent-color);
    box-shadow: 0 0 0 2px var(--accent-color);
  }

  .card-cta {
    background: var(--accent-color);
  }
}

No class mutations from JavaScript, no prop drilling through a component tree — just CSS reacting to CSS. This is particularly powerful in design systems where a theme context needs to propagate down without coupling components to a specific parent structure.

Gotcha: Style queries are newer and browser support, while growing, is slightly behind size queries. Check caniuse.com before shipping them to older enterprise targets. As of mid-2026, Chrome, Firefox, and Safari all support them, but make sure your QA process covers the versions you actually support.


@container vs Media Queries: When to Use Each

This isn’t a "use container queries for everything" argument. Both tools have a place.

Use media queries for:

  • Page-level layout changes (switching from single to multi-column)
  • Global typography scale adjustments
  • Navigation pattern changes (hamburger menu threshold)
  • Anything tied to device capabilities or viewport semantics (print styles, orientation)
  • Body-level font-size adjustment

Use container queries for:

  • Any reusable UI component (cards, tiles, tables, forms, charts)
  • Components that appear in more than one layout context
  • Design system primitives
  • Dashboard widgets
  • Anything a developer will drop into an arbitrary slot and expect it to "just work"

The clean mental model: media queries own the page, container queries own the component.


Gotchas You’ll Hit in Production

1. The wrapper div problem

Every container query requires a wrapper element that you control. Sometimes that wrapper is already in your markup. Sometimes it’s not, and adding one introduces layout side effects — especially with flex or grid children, where adding an extra div breaks implicit sizing.

The fix: CSS Grid’s subgrid or a single-element container pattern using ::before/::after as the query target ancestor is not available. You need a real DOM element. Accept the wrapper, but be deliberate about it. Use display: contents on the wrapper if you need it to be transparent to layout:

.card-wrapper {
  container-type: inline-size;
  display: contents; /* transparent to flex/grid layout */
}

Careful: display: contents removes the element from the accessibility tree in some older browser versions, and it’s been a buggy area historically. Test with your actual browser targets.

2. Container queries and initial layout

The container query fires based on the computed size after layout. On the very first paint, the browser may need to resolve the container’s size before it can apply the query styles. In practice this is not visible to users, but it means you can’t rely on container queries for above-the-fold critical path rendering that needs zero layout recalculation. Use base styles that are reasonable for the smallest expected container, then let queries enhance.

3. The percentage inside a container

Inside a container context, % units on the queried element still refer to the containing block, not the container itself. Don’t confuse cqi (container query inline units) with percentage widths. The new cqi, cqb, cqw, cqh, cqmin, cqmax units resolve relative to the container:

@container card (min-width: 400px) {
  .card-image {
    width: 40cqi; /* 40% of the container's inline size */
  }
}

These are cleaner than percentages when building truly container-relative layouts.

4. Don’t nest container queries mindlessly

Each container establishes a new stacking context for queries. Deeply nested containers with conflicting query names are a debugging nightmare. Keep the hierarchy flat: one container per distinct component boundary. If a sub-component inside a component also needs queries, give it its own named container — but don’t create a third level unless you genuinely need it.

5. JavaScript resizing and container queries

If you’re programmatically resizing a container (animation, drag resize, accordion), the browser fires container query recalculations continuously. This is fine in modern browsers, but watch for jank with complex query-driven layouts during rapid resizes. The browser is smart about this, but complex @keyframes combined with container query changes can cause stutters. Profile first before blaming the feature.


Production-Ready Setup for a Design System

Here’s how to structure container queries in a real design system where components need to be portable:

/* tokens.css — define query breakpoints as custom props for consistency */
:root {
  --cq-sm: 320px;
  --cq-md: 480px;
  --cq-lg: 640px;
  --cq-xl: 800px;
}

Since @container doesn’t support var() in the query condition directly, use a PostCSS plugin or CSS custom media queries as a build step if you want centralized breakpoint tokens. Alternatively, document your standard breakpoints and stick to them — consistency matters more than DRY in this case.

/* component-base.css — every component wrapper follows this pattern */
[data-container] {
  container-type: inline-size;
}

/* Name is set per-component */
[data-container="card"] {
  container-name: card;
}

[data-container="widget"] {
  container-name: widget;
}
<!-- Usage: the wrapper is declarative, not structural -->
<div data-container="card">
  <div class="product-card">...</div>
</div>

This keeps the containment setup out of component CSS and makes it obvious in markup that this element is a container boundary. It also means you can add or rename containers without touching component styles.


Testing Container Queries

Browser devtools now support container query inspection. In Chrome DevTools, the Elements panel shows a "container" badge on elements with container-type set. Hover it to see an overlay of the container’s tracked dimensions.

For automated testing: if you’re using Playwright or Cypress for visual regression, container queries are invisible to the testing framework — the browser handles them. Just size your test viewport to a value that triggers the breakpoints you want to exercise, or better yet, size the container element directly:

// Playwright: resize a specific element to test container query behavior
await page.evaluate(() => {
  document.querySelector('.card-wrapper').style.width = '350px';
});
await expect(page.locator('.product-card')).toHaveCSS('flex-direction', 'column');

await page.evaluate(() => {
  document.querySelector('.card-wrapper').style.width = '600px';
});
await expect(page.locator('.product-card')).toHaveCSS('flex-direction', 'row');

Browser Support

As of mid-2026: Chrome 105+, Firefox 110+, Safari 16+. Edge follows Chrome. This covers well over 95% of global usage. There’s no longer a serious argument against shipping container queries in production unless your analytics show significant traffic from genuinely old browsers.

For the tail case: container query failures are graceful. If the browser doesn’t support them, the base (unqueried) styles apply. Write your base styles for the narrowest reasonable context and the layout degrades sanely on unsupported browsers. No JavaScript fallback needed, no Intersection Observer hacks.


The Bigger Picture

The switch from media queries to container queries isn’t just a CSS trick — it’s a shift in how you model component responsibility. A component should know how to behave in its own space. It should not depend on a global signal (viewport width) that has nothing to do with its context.

This matters most when you’re working in a team or maintaining a design system that other developers consume. Components with container queries are predictably portable. Developers can drop them into layouts they haven’t built yet and trust the styling handles itself. That’s a real reduction in integration bugs, and it makes the design system actually worth using.

The tooling is ready. The browser support is there. The only thing left is to actually change how you write component styles.

Start with the next component you build. Define a named container on the wrapper, write the base styles mobile-first, add a single container query for the wider breakpoint. That’s the full migration cost for a new component. For existing components, refactor opportunistically — when a component breaks in a new layout context, that’s the moment to replace its media queries with container queries. Don’t do a big-bang rewrite.

The goal isn’t to eliminate media queries. It’s to use the right tool. Page layout is still the viewport’s job. Everything inside it is the component’s job. @container finally gives components the tools to do it.

Leave a comment

👁 Views: 24,119 · Unique visitors: 34,994