CSS Container Queries

Published 5 Jun 2026

A practical guide to CSS container queries, how they differ from media queries, when to use container-type, and how to build components that respond to their own space.

Responsive design used to mostly mean one thing: look at the viewport.

If the screen is narrow, stack the layout. If the screen is wide, add columns. If the screen crosses a breakpoint, change the navigation, card grid, sidebar, or typography.

That model still matters. Media queries are not going away.

But they answer a page-level question:

How wide is the viewport?

A lot of component problems need a different question:

How much space does this component have?

That is what CSS container queries are for.

They let a component respond to the size of its parent container instead of the size of the browser window. That sounds like a small shift, but it changes how you build reusable UI.

A card can be compact in a sidebar and spacious in a main content column. A toolbar can wrap when its own container gets tight. A media object can switch from stacked to horizontal based on the space it actually has, not based on some global page breakpoint.

If responsive CSS still feels like a pile of one-off fixes, start with the broader CSS Survival Guide: The 20% That Solves 80% of Layout Problems. Container queries fit into that same mental model: make the parent responsible for layout, then let children adapt inside clear boundaries.


The old problem with media queries

Imagine a product card.

On a wide page, the card looks good with the image on the left and the content on the right:

[ image ]  Product name
           Price
           Button

On a narrow page, the card should stack:

[ image ]
Product name
Price
Button

With media queries, you might write:

.product-card {
  display: grid;
  gap: 1rem;
}

@media (min-width: 700px) {
  .product-card {
    grid-template-columns: 160px 1fr;
    align-items: center;
  }
}

That works if the card always lives in the same kind of page layout.

But real components move around.

The same card might appear in:

  • a full-width product list
  • a narrow sidebar
  • a carousel
  • a dashboard column
  • a related-products grid
  • a modal

The viewport might be 1200px wide, but the card itself might only have 280px of space.

A media query cannot see that.

It only sees the page.

So the card may switch to the “wide” layout because the browser window is wide, even though the card's actual container is cramped. The result is a layout that is technically responsive, but not locally responsive.


Container queries change the question

With a container query, the component can respond to the space around it.

First, create a query container:

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

Then query that container:

.product-card {
  display: grid;
  gap: 1rem;
}

@container (min-width: 36rem) {
  .product-card {
    grid-template-columns: 160px 1fr;
    align-items: center;
  }
}

Now the breakpoint belongs to the component's available space.

If the card is inside a narrow sidebar, it stays stacked.

If the card is inside a wider content area, it switches to the horizontal layout.

The viewport no longer has to predict every place the component might be used.

That is the main benefit of container queries: components can become responsive to context.


The container is not usually the thing you style

This is the first gotcha.

You do not usually query and restyle the same element.

A container query styles descendants based on an ancestor container. That means this is the common shape:

<div class="card-container">
  <article class="card">
    <img src="product.jpg" alt="" />
    <div>
      <h2>Product name</h2>
      <p>Short description.</p>
    </div>
  </article>
</div>
.card-container {
  container-type: inline-size;
}

.card {
  display: grid;
  gap: 1rem;
}

@container (min-width: 36rem) {
  .card {
    grid-template-columns: 12rem 1fr;
  }
}

The .card-container is being measured.

The .card is being changed.

That separation matters because containment is a contract. When you say an element is a container, the browser needs to be able to calculate its size without its children creating a circular dependency.

In plain English:

The child can respond to the parent.
The parent should not depend on the child responding.

That is why wrapper elements are common with container queries. They give the browser something stable to measure.


Use inline-size first

Most of the time, start here:

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

inline-size means the query is based on the container's inline direction.

In a normal horizontal writing mode, that usually means width.

That is what most responsive component decisions need:

  • does this card have enough width for two columns?
  • does this toolbar have enough width to keep items in one row?
  • does this form have enough width for label/input pairs?
  • does this navigation section have enough width for expanded labels?

There is also container-type: size, which allows queries against both dimensions. But it applies stronger size containment and is easier to misuse.

For everyday component layout, inline-size is the boring, useful default.


Naming containers makes queries less fragile

A simple @container query uses the nearest eligible ancestor container.

That is often fine:

@container (min-width: 36rem) {
  .card {
    grid-template-columns: 12rem 1fr;
  }
}

But in real interfaces, components can be nested inside other components. You may have a card inside a panel inside a dashboard inside a page shell. More than one ancestor might be a container.

When the relationship matters, name the container:

.card-container {
  container-name: product-card;
  container-type: inline-size;
}

Or use the shorthand:

.card-container {
  container: product-card / inline-size;
}

Then target that named container:

@container product-card (min-width: 36rem) {
  .card {
    grid-template-columns: 12rem 1fr;
  }
}

This makes the CSS more explicit.

The rule now says:

When the product-card container is wide enough, use the wider card layout.

That is easier to understand than hoping the nearest container is always the one you meant.


Container queries are not replacements for media queries

Container queries solve component-level problems.

Media queries still solve environment-level problems.

Use media queries for things like:

  • viewport-wide page layout
  • global navigation changes
  • motion preferences
  • color scheme preferences
  • pointer and hover capabilities
  • print styles

Use container queries for things like:

  • cards
  • toolbars
  • sidebars
  • panels
  • form groups
  • reusable content blocks
  • components that appear in different layout contexts

A useful rule of thumb:

Media query     = respond to the browser or device
Container query = respond to the component's available space

For example, a page shell might still use a media query:

.page {
  display: grid;
  gap: 2rem;
}

@media (min-width: 60rem) {
  .page {
    grid-template-columns: 16rem 1fr;
  }
}

Inside that page shell, individual components can use container queries:

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

.summary-card {
  display: grid;
  gap: 1rem;
}

@container panel (min-width: 32rem) {
  .summary-card {
    grid-template-columns: auto 1fr;
  }
}

The page responds to the viewport.

The component responds to its slot inside the page.

That combination is usually cleaner than trying to make one global breakpoint system handle everything.


A practical card pattern

Here is a complete card example.

The wrapper establishes the container:

<div class="article-card-container">
  <article class="article-card">
    <img class="article-card__image" src="cover.jpg" alt="" />
    <div class="article-card__body">
      <p class="article-card__eyebrow">CSS</p>
      <h2 class="article-card__title">Container queries make components more portable</h2>
      <p class="article-card__summary">The card can adapt to the space it has instead of guessing from the viewport.</p>
    </div>
  </article>
</div>

The default layout is narrow-first:

.article-card-container {
  container: article-card / inline-size;
}

.article-card {
  display: grid;
  gap: 1rem;
}

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

.article-card__title {
  margin: 0;
}

Then the card becomes horizontal when the container has enough space:

@container article-card (min-width: 38rem) {
  .article-card {
    grid-template-columns: 14rem 1fr;
    align-items: center;
  }

  .article-card__image {
    height: 100%;
    aspect-ratio: 4 / 3;
  }
}

This card can now live in several places without needing new page-level breakpoints.

In a narrow column, it stacks.

In a wide column, it becomes horizontal.

In a grid where each card gets medium space, each card makes the decision for itself.

That last part is important. If different cards have different available widths, they can adapt independently.


Container queries work especially well with grid

Grid already lets the parent decide how much space each child receives.

Container queries let each child respond to the space it received.

For example:

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: 1.5rem;
}

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

The grid controls the columns.

Each .card-shell becomes a query container.

Then the card can adapt inside its assigned column:

.card {
  display: grid;
  gap: 1rem;
}

@container card (min-width: 28rem) {
  .card {
    grid-template-columns: 8rem 1fr;
  }
}

This is a powerful pattern because the grid does not need to know the details of the card.

The grid says:

Here is your space.

The card says:

Given this much space, here is my best layout.

That separation keeps CSS from turning into a pile of page-specific exceptions.


Container query units are part of the story

Container queries also come with container-relative units.

You may see units like:

.card-title {
  font-size: clamp(1rem, 6cqi, 1.75rem);
}

The cqi unit means one percent of the query container's inline size.

In normal horizontal writing, it behaves roughly like “one percent of the container width.”

There are other container query units too:

cqw = 1% of the container width
cqh = 1% of the container height
cqi = 1% of the container inline size
cqb = 1% of the container block size
cqmin = the smaller cqi or cqb value
cqmax = the larger cqi or cqb value

These are useful when a value should scale with a component, not the viewport.

For example:

.hero-card {
  container: hero-card / inline-size;
  padding: clamp(1rem, 5cqi, 3rem);
}

That padding now responds to the container's size.

Use this carefully. A little fluid sizing can make a component feel more natural. Too much can make the design feel unstable.

The same rule applies to viewport units: just because a unit is powerful does not mean every value should be fluid.


Do not container-query everything

Container queries are a layout tool, not a requirement.

If a component only appears in one place, a media query or normal grid/flex behavior may be simpler.

If the component adapts naturally with wrapping, gap, minmax(), auto-fit, or flex-wrap, you may not need a container query at all.

For example, this cluster pattern often needs no breakpoint:

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

And this grid pattern can handle many card layouts without custom queries:

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 1rem;
}

Reach for container queries when the component needs a meaningful layout change based on the space it has.

Not every width change needs a breakpoint.

Sometimes the browser's normal layout algorithms already solve the problem.


Watch for overflow inside queried components

Container queries make components more flexible, but they do not remove normal CSS layout rules.

If a component uses flex or grid internally, it can still hit the classic shrinking problem.

For example:

.card-row {
  display: flex;
  gap: 1rem;
}

.card-row__content {
  flex: 1;
}

If the content includes a long title, URL, or code-like string, the row may overflow.

You may still need:

.card-row__content {
  flex: 1;
  min-width: 0;
}

Container queries decide when a component should change layout.

They do not decide how overflowing content should wrap, truncate, or scroll.

That is still your job.

For the deeper version of this problem, read Why Flex Items Refuse to Shrink and The Hidden Complexity of CSS Overflow.


Think in component contracts

The cleanest container-query code usually has a clear contract.

For a reusable component, ask:

What space does this component need for each layout?

Not:

What screen size is this component probably on?

For a card, that contract might be:

Below 30rem: stack image and text.
Above 30rem: place image and text side by side.

For a toolbar:

Below 24rem: wrap actions.
Above 24rem: keep actions in one row.

For a form group:

Below 34rem: stack label and field.
Above 34rem: use two columns.

Those breakpoints are about the component, not the page.

That makes them easier to test. You can resize the component in isolation and see whether the layout decisions make sense.

It also makes them easier to reuse. The component carries its responsive behavior with it.


A debugging checklist

When a container query does not work, check these in order:

  1. Is there an ancestor with container-type set?
  2. Are you styling a descendant of the container, not the container itself?
  3. Is the query looking at the right axis?
  4. Would inline-size be enough instead of size?
  5. If there are nested containers, should this container be named?
  6. Is the breakpoint based on the component's actual usable space?
  7. Is normal flex or grid behavior already enough?
  8. Is overflow caused by content rather than the query?
  9. Are you using container units where a fixed or clamped value would be clearer?
  10. What does DevTools show as the container's actual size?

Most bugs come from one of two mistakes:

There is no valid container to query.

Or:

The query is attached to the wrong level of the component.

Find the container. Find the descendant being styled. Then check the measured size.


The rule of thumb

Use media queries when the page needs to respond to the viewport.

Use container queries when a component needs to respond to its own available space.

Start with this:

.component-shell {
  container: component / inline-size;
}

.component {
  display: grid;
  gap: 1rem;
}

@container component (min-width: 32rem) {
  .component {
    grid-template-columns: auto 1fr;
  }
}

Then ask whether the breakpoint belongs to the page or the component.

That question is the whole mental shift.

Responsive design is no longer only about the browser window.

It is about the space a piece of UI actually gets.

Once you start thinking that way, container queries stop feeling like a fancy new syntax and start feeling like the missing piece that makes component-based CSS make sense.