A card looks fine in the main content column. Move it into a sidebar on the same desktop screen and its image pushes the text into an unusably narrow strip. The viewport has not changed, so the media query that selected the horizontal layout has not changed either.

A container size query solves that mismatch: it lets a descendant change styles according to the available size of an eligible ancestor, not just the viewport. The important detail is eligible ancestor. A container query does not automatically measure the element being styled, and it does not necessarily measure the immediate parent.

This guide builds one reusable card, checks it in two columns at the same viewport width, and follows the failure cases that are easy to miss when adopting @container.

Viewport breakpoints and component breakpoints answer different questions

Consider this rule:

.product-card {
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 800px) {
  .product-card {
    grid-template-columns: 12rem minmax(0, 1fr);
  }
}

At a 1200px viewport, every matching card gets the two-column layout, including one placed inside a 300px sidebar. The rule knows nothing about how much space that particular card received.

A media query remains useful for page-level decisions such as switching a site navigation layout or responding to prefers-reduced-motion. A container query is a better fit when a reusable component must react to the width of a dashboard panel, sidebar, modal, or grid cell.

QuestionRelevant tool
Is the browser viewport wide enough for the site navigation?@media
Is this card's containing region wide enough for an image beside its text?@container
Can the grid fit another column without a discrete breakpoint?repeat(auto-fit, minmax(...)), when intrinsic Grid layout is sufficient

The query is not a replacement for Grid or Flexbox. It decides when a component should switch between layouts; Grid and Flexbox still perform the layout.

Build the same card in a main column and a sidebar

Save the following as container-card.html and open it in a browser that supports container size queries. It contains no framework or JavaScript dependency.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Container query reproduction</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; padding: 24px; font: 16px/1.5 system-ui, sans-serif; }

    .layout {
      display: grid;
      grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
      gap: 16px;
    }

    .slot {
      container: card-slot / inline-size;
      min-width: 0;
    }

    .product-card {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      gap: 16px;
      border: 1px solid #777;
      padding: 16px;
      border-radius: 12px;
    }

    .product-card__visual {
      background: #dbeafe;
      min-height: 120px;
      border-radius: 8px;
    }

    .product-card__body { min-width: 0; }
    .product-card h2 { margin: 0 0 8px; font-size: 1.25rem; }
    .product-card p { margin: 0; overflow-wrap: anywhere; }

    @container card-slot (min-width: 34rem) {
      .product-card {
        grid-template-columns: 12rem minmax(0, 1fr);
        align-items: center;
      }
    }

    /* Page-level columns still depend on the viewport. */
    @media (max-width: 42rem) {
      .layout {
        grid-template-columns: minmax(0, 1fr);
      }
    }
  </style>
</head>
<body>
  <div class="layout">
    <section class="slot" id="main-slot" aria-label="Main results">
      <article class="product-card" id="main-card">
        <div class="product-card__visual" aria-hidden="true"></div>
        <div class="product-card__body">
          <h2>Reusable product card</h2>
          <p>Designed for a main column or a sidebar.</p>
        </div>
      </article>
    </section>

    <aside class="slot" id="side-slot" aria-label="Sidebar results">
      <article class="product-card" id="side-card">
        <div class="product-card__visual" aria-hidden="true"></div>
        <div class="product-card__body">
          <h2>Reusable product card</h2>
          <p>Designed for a main column or a sidebar.</p>
        </div>
      </article>
    </aside>
  </div>
</body>
</html>

There are four decisions worth noticing.

  1. The slot is the query container. container: card-slot / inline-size is shorthand for container-name: card-slot and container-type: inline-size. The card is a descendant of that slot, so its CSS can query the slot.
  2. The narrow layout comes first. One grid column works even when the query is unsupported or the slot is small. The two-column layout is an enhancement for containers with at least 34rem of content-box width. This threshold is a layout decision, not a device category.
  3. The grid children can shrink. minmax(0, 1fr), min-width: 0 and text wrapping handle different constraints. Container queries cannot fix a long, unbreakable string or an intrinsic minimum-width problem by themselves. The box model and overflow debugging guide examines those failures separately.
  4. The outer page grid also needs a viewport breakpoint. Below 42rem, the main column and sidebar stack instead of squeezing the sidebar card into an unusably narrow slot. That is a page-level decision for @media; each card still switches its internal layout with @container.

The example uses a decorative rectangle rather than a real product image, so image loading does not affect the test. In a production component, supply meaningful alt text for informative images rather than hiding them from assistive technology.

Verify that the container, not the viewport, controls the card

In a headless Chromium 144 reproduction at a 1200px viewport, the layout above produced these measurements:

StateMain slotSidebar slotMain cardSidebar card
Original layout757px379pxTwo columnsOne column
Layout constrained to 660px, viewport unchanged429px215pxOne columnOne column

The slot widths above are rounded border-box measurements; the query itself compares the container's content box. Here, the slot has no padding or border, so that distinction does not change which rule matches. These are observations from this small fixture, not performance measurements or a cross-browser compatibility test.

To reproduce the second row without changing the browser window, open DevTools and temporarily add max-width: 660px to .layout while keeping the viewport at 1200px. The main card switches to one column. Remove the declaration and it returns to two columns. This test changes the available container width without activating the viewport media query.

The page-level breakpoint was checked independently in Chromium 144 at viewport widths of 320, 390, 660, 672, 673, and 1200 CSS pixels. At 672px and below the outer columns stack, and neither card nor the document overflows horizontally in this fixture. Above 672px, the outer layout uses two columns, while the cards still respond to their respective container widths. The test does not establish a general guarantee for different text lengths, font settings, or component content.

Two further checks distinguish a working query from a misleading CSS rule:

  • Remove container: card-slot / inline-size from .slot. Neither card satisfies the named size query; both keep the narrow default, even though the main slot is wide.
  • Try writing a query intended to change .slot itself. A container's size query styles descendants, not the queried element. Put the query container on an ancestor wrapper if the element you need to restyle is currently the container.

This explains why @container is not simply @media with a different keyword. The component needs a queryable ancestor.

Which container does the browser actually measure?

With an unnamed size query, the browser selects the nearest ancestor eligible for the requested size features. That may be a different ancestor than the one you expected in a nested component tree.

For example:

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

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

/* For .card inside .card-shell, this queries .card-shell,
   not the more distant .dashboard-panel. */
@container (min-width: 30rem) {
  .card { /* wider layout */ }
}

Give containers names when the relationship needs to be explicit:

.dashboard-panel {
  container: dashboard / inline-size;
}

.card-shell {
  container: card-slot / inline-size;
}

@container dashboard (min-width: 50rem) {
  .card { /* condition uses the named dashboard ancestor */ }
}

A name does not make any element queryable for size. The named ancestor still needs a compatible container-type, and the styled element must still be its descendant.

inline-size is usually a reasonable starting point for width-driven components. It queries the logical inline axis, which corresponds to width in common horizontal writing modes. size makes both axes available for size queries but also imposes stronger size containment. An element whose block size relies on its contents can behave unexpectedly under that constraint. Do not select size just because it sounds more complete.

The MDN container queries guide documents the sizing modes and naming rules, and the @container reference describes the condition syntax.

Four reasons a container query appears not to work

SymptomWhat to inspectLikely correction
The wide rule never appliesCheck container-type and the element's ancestorsPut inline-size on an ancestor, not only the card
The wrong panel controls the ruleInspect nested eligible containers and their namesUse a specific container-name where appropriate
The measured width seems differentCompare content-box width with outer dimensions and paddingChoose the threshold using the queried content box
The rule matches but the text overflowsInspect Grid/Flex minimum sizes and long tokensFix shrinking and wrapping independently

In Chromium-based DevTools, inspect the card in the Elements panel, find the matching @container rule, and follow its link to the queried ancestor. The container badge can also display the relationship between a query container and its descendants. Google's container query debugging instructions show the inspection workflow.

Choose a breakpoint from the component's minimum usable layout width, not from a familiar phone or tablet width. For this example, the horizontal version reserves 12rem for the visual, plus the card's padding, gap, and a usable width for text. If your real content is longer or localized, retest and adjust the threshold.

Container units are useful, but not a substitute for constraints

Container-relative units let descendants scale with a size query container:

  • 1cqi equals 1% of its inline size.
  • 1cqw equals 1% of its width.
  • cqb and cqh refer to block size and height.
  • cqmin and cqmax select the smaller or larger of the relevant logical dimensions.

For example:

.card-title {
  font-size: clamp(1.125rem, 1rem + 1cqi, 1.5rem);
}

This sets a bounded font size that responds to an eligible query container's inline dimension. The clamp() limits still matter: fluid text without upper and lower bounds may become too small or too large.

There is a subtle fallback. If no eligible size container is found for an axis, the corresponding container-relative unit resolves using a small viewport unit for that axis. It does not simply become zero. The MDN length reference documents this behavior.

That fallback is another reason to inspect the ancestor chain when typography seems to react to the viewport instead of its intended panel.

Do you still need media queries or JavaScript?

Yes, depending on the question.

Use @media for global viewport conditions and user preferences. Use intrinsic Grid or Flexbox layout when its normal wrapping behavior already handles the job. Use @container when a component needs a conditional style change tied to an ancestor's size.

Container queries also include capabilities beyond size queries, including style queries. The MDN size and style query guide distinguishes them. Do not assume that a browser supporting size queries also supports querying arbitrary CSS declarations with style(): support for style features is more limited, with custom-property queries being the interoperable focus of current guidance.

For a component library targeting older browsers or embedded webviews, test its actual support matrix. A narrow-first layout gives a usable fallback in environments that ignore the enhancement. Feature detection such as CSS.supports('container-type', 'inline-size') checks whether the property value is recognized; it is not a substitute for exercising the component's real layout and relevant query syntax.

The decision matters in a reusable design system: the component should expose a clear contract about the space it needs rather than silently depending on a particular page template.

Before adopting a query, put the same component in a wide content area, a narrow sidebar, and a nested panel. Keep the viewport fixed while resizing the surrounding region. If each instance switches only when its own usable space crosses the intended threshold, you have tested the behavior that viewport media queries could not express.