Back to Blog

Container Queries: Components That Know Their Size

The Problem With Media Queries at Component Level

A product card in a three-column grid has about 300 pixels of available width. The same card component in a single-column full-width slot has 800 pixels. A media query cannot tell which situation applies — the viewport is identical in both cases. So you end up writing override rules for each placement, or you reach for JavaScript to measure the element and toggle classes at runtime.

Container queries solve this precisely. Instead of asking the viewport, they ask the container: how wide are you right now? This is the fourth post in the native web series, following <dialog> for modals, the Popover API for menus and tooltips, and native form validation. The pattern is the same: the platform grew a proper primitive, and the workaround you have been shipping can go.

Browser Support

CSS container size queries are Baseline Widely Available, supported in Chrome 105, Edge 105, Firefox 110, and Safari 16 — all shipping between August 2022 and February 2023. Global coverage sits at approximately 96% as of 2026. No polyfill is required.

The Two-Step Setup

Using container queries requires two things: declare a container, then write styles for its children.

Step 1 — declare the container:

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

container-type: inline-size tells the browser this element can be queried on its inline axis (width). That is the value you want for the vast majority of layouts. The container becomes queryable, and any descendant element can have styles that apply conditionally based on the container's measured width.

You can also give the container a name, which is useful when multiple nested containers exist:

.card-wrapper {
  container-type: inline-size;
  container-name: card;
  / or shorthand: container: card / inline-size; /
}

Step 2 — write container query styles for children:

@container (min-width: 480px) {
  .card {
    flex-direction: row;
    align-items: center;
  }
  .cardimage {
    width: 200px;
    flex-shrink: 0;
  }
}

/ Targeting a named container /
@container card (min-width: 480px) {
  .cardtitle {
    font-size: 1.5rem;
  }
}

The unnamed form targets the nearest ancestor with any container-type set. The named form targets the nearest ancestor with that specific container name, letting you skip intermediate containers when nesting.

A Complete Card Example

Here is the full pattern for a card that switches between stacked and horizontal layouts based on its container:

<div class="card-wrapper">
  <article class="card">
    <img class="cardimage" src="product.jpg" alt="Product name">
    <div class="cardbody">
      <h2 class="cardtitle">Product Name</h2>
      <p class="carddescription">A short description of the product.</p>
      <a href="/product" class="cardcta">View details</a>
    </div>
  </article>
</div>

<style>
  .card-wrapper {
    container-type: inline-size;
  }

  / Base styles — narrow/stacked layout /
  .card {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    overflow: hidden;
  }

  .cardimage {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }

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

  / Wide layout — kicks in when the container has room /
  @container (min-width: 480px) {
    .card {
      flex-direction: row;
    }
    .cardimage {
      width: 200px;
      aspect-ratio: auto;
      flex-shrink: 0;
    }
  }
</style>

Drop this card anywhere on your site. In a narrow sidebar at 260px, it stacks. In a main content area at 800px, it switches to horizontal. No JavaScript, no per-placement CSS overrides, no ResizeObserver.

Container Query Units

Container queries ship with a set of length units relative to the container's dimensions:

  • cqw — 1% of the container's width
  • cqh — 1% of the container's height
  • cqi — 1% of the container's inline size
  • cqb — 1% of the container's block size

The most practical use is fluid typography that scales with the component, not the viewport. Pair with clamp() to set a minimum and maximum:

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

.cardtitle {
  font-size: clamp(1rem, 3cqi + 0.5rem, 2rem);
}

The title grows as the card's container grows, capped at 2rem. This works per component instance, so a card in a wide slot gets a larger title than the same card in a narrow sidebar, without writing two separate rules.

What This Replaces in JavaScript

Before container queries, component-level responsiveness typically required a ResizeObserver:

const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    const width = entry.contentRect.width;
    entry.target.classList.toggle('card--wide', width >= 480);
    entry.target.classList.toggle('card--narrow', width < 480);
  }
});

document.querySelectorAll('.card-wrapper').forEach(el => observer.observe(el));

Then in CSS, each variant was a separate class rule. The container query version replaces the observer setup, the class toggling, and the two variant class rules with a single @container block. There are no timing concerns, no layout shifts from the observer firing after initial paint, and no JavaScript to load and wire up.

The Sidebar and Main Column Problem, Solved

The canonical argument for container queries over media queries is exactly this scenario. You have a component that appears in two places on the same page: a narrow sidebar and a wide main area. With media queries, there is no breakpoint that correctly identifies which slot the component is in, because the breakpoint is viewport-width, not component-width. You end up with placement-specific overrides:

/ The old approach — fragile, placement-specific /
@media (min-width: 900px) {
  .sidebar .card { / narrow styles / }
  .main-area .card { / wide styles / }
}

This coupling between context and component breaks whenever the layout changes. Container queries decouple the component from its placement entirely:

/ Both contexts declare themselves as containers /
.sidebar, .main-area {
  container-type: inline-size;
}

/ The component responds to whatever container it finds itself in /
@container (min-width: 480px) {
  .card {
    flex-direction: row;
  }
}

Move the card to a different part of the page and the styles follow the container's actual width. No overrides needed.

Gotchas

The container cannot query itself. Only descendants respond to a container query. You cannot write @container styles that target the container element itself. The container must wrap the element you want to style.

Do not apply container-type to the element you plan to style. The containment and the responsive child must be separate elements. A common mistake is adding container-type to .card and then writing @container styles targeting .card — it does not work. Add a wrapper.

container-type: size requires a definite height. Querying the block axis means the container must have an explicit height, or it breaks intrinsic sizing. Use inline-size for the common case. Only reach for size if you genuinely need to query height.

Nesting works, but name your containers. When containers are nested, an unnamed @container query targets the nearest ancestor container. If you have a card inside a grid inside a page wrapper and all three are containers, your card's query hits the grid, not the wrapper. Name your containers to be explicit about which ancestor you are querying.

Start Here

Container queries are production-ready. Ninety-six percent global coverage, Baseline Widely Available since 2023, no polyfill. MDN's container query guide covers the full specification. Ahmad Shadeed's interactive guide to container queries is the best visual walkthrough of the containment model. The web.dev post on stable container query support documents exactly when each browser shipped.

Copy the card example above, add the container-type to the wrapper you already have, and replace your first ResizeObserver with an @container block.

Next in this series: CSS :has() — the relational pseudo-class that models UI state without JavaScript. Tabs, highlights, toggles, and conditional parent styling, all in CSS.