# CSS Container Queries

> CSS container queries style a component by its own width, not the viewport. Learn container-type, named containers, query units, and the traps that break layouts.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-17 | Updated: 2026-08-16 | Topics: [CSS](https://flaviocopes.com/tags/css/) | Canonical: https://flaviocopes.com/css-container-queries/

A card does not care how wide the browser window is. It cares how wide the hole you dropped it into is.

That hole might be a narrow sidebar. Or a wide main column. Or one cell in a three-column grid. The viewport can stay the same while the card's available space changes.

[Media queries](https://flaviocopes.com/css-media-queries/) look at the viewport. That is the right tool for the page: hide a nav, switch a 3-column grid to 1 column, change the type size of the whole article.

It is the wrong tool for a reusable component. If you write `@media (min-width: 700px)` on a card, that card also "goes wide" when it sits in a 240px sidebar on a large screen. The window is wide. The card is not.

Container queries fix that. You mark a wrapper as a **container**. Then you write rules that look at *that wrapper*, not the window.

I use them whenever a component has to survive more than one slot. I keep media queries for the page around it.

## Mark a wrapper as a container

You do not query an element from itself. You query an ancestor.

Give the wrapper `container-type: inline-size`. That tells the browser: track this element's width, because something inside will ask about it.

```css
.card-slot {
  container-type: inline-size;
}
```

`inline-size` is the value you want almost every time. In a horizontal writing mode, inline size is the width.

There is also `size`, which tracks width *and* height. Skip it unless you truly need a height query. `size` applies size containment on both axes. A box that used to grow with its content can collapse, because the browser is no longer allowed to let descendants define that height.

`normal` is the default. The element is not a size container.

The `container` shorthand sets a name and a type in one line:

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

That is the same as `container-name: card` plus `container-type: inline-size`. We will use the name in a minute.

## Query the container, not the viewport

`@container` is the cousin of `@media`. The condition looks similar. The thing being measured is different.

```css
.card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

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

When the nearest size container is at least 400px wide, the card goes side by side. When it is narrower, the card stacks. Resize the sidebar. The viewport can stay put.

You can also write the newer range syntax:

```css
@container (width > 400px) {
  .card {
    flex-direction: row;
  }
}
```

Same idea. I still use `min-width` a lot because it matches how I already write media queries. Either form is fine.

Conditions measure the container's **content box**. Padding on the wrapper is not part of the number.

## The query styles descendants, not the container

This is the rule that surprises people.

`@container` applies to elements *inside* the container. It does not style the container itself.

This does nothing useful:

```css
.card-slot {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card-slot {
    padding: 2rem;
  }
}
```

`.card-slot` is the container. It is not a descendant of itself. The rule never matches.

Put the query on the component inside:

```html
<div class="card-slot">
  <article class="card">
    <img src="/images/newsletter.jpg" alt="">
    <div class="card-body">
      <h2>Weekly notes</h2>
      <p>One email on shipping software.</p>
    </div>
  </article>
</div>
```

```css
.card-slot {
  container-type: inline-size;
}

.card {
  display: flex;
  flex-direction: column;
}

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

  .card img {
    width: 10rem;
    object-fit: cover;
  }
}
```

That is why a dedicated wrapper is worth it. The slot is the container. The card is the thing that changes.

This pairs well with [CSS Grid](https://flaviocopes.com/css-grid/) for the page and [Flexbox](https://flaviocopes.com/flexbox/) inside the card. Grid decides how wide each slot is. The container query decides how the card uses that width.

## A card that works in two slots

Here is the situation I actually hit. Same card. Two places on one page.

```html
<div class="page">
  <main class="main">
    <div class="card-slot">
      <article class="card">...</article>
    </div>
  </main>
  <aside class="sidebar">
    <div class="card-slot">
      <article class="card">...</article>
    </div>
  </aside>
</div>
```

```css
.page {
  display: grid;
  grid-template-columns: 1fr 16rem;
  gap: 2rem;
}

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

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

@container (min-width: 400px) {
  .card {
    flex-direction: row;
    align-items: center;
  }
}
```

On a wide window the main column is well above 400px, so that card goes horizontal. The sidebar is 16rem, so that card stays stacked.

No extra class. No `card--compact`. No JavaScript measuring the node. The same HTML works in both slots.

If you only have one layout, and the card always spans the page, a media query is enough. Do not add a container for sport.

## Named containers

Without a name, `@container` walks up to the nearest size container.

That is fine until you nest them. A widget inside a card inside a sidebar can accidentally listen to the card, not the sidebar.

Name the one you mean:

```css
.sidebar {
  container: sidebar / inline-size;
}

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

@container sidebar (min-width: 300px) {
  .widget {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}
```

The widget now ignores the card container above it. It only asks the `sidebar` container.

You can also write a name-only query, with no size condition:

```css
@container sidebar {
  .widget-title {
    font-size: 0.85rem;
  }
}
```

That just means "this element lives inside a container named sidebar." I rarely need that. Size queries are the ones that pay rent.

## Container query units

`vw` is 1% of the viewport width. Container units are the same idea, pointed at the container.

| Unit | Meaning |
| --- | --- |
| `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 of `cqi` and `cqb` |
| `cqmax` | the larger of `cqi` and `cqb` |

I use `cqi` most. It follows writing mode, like `inline-size`.

```css
.card-title {
  font-size: clamp(1rem, 4cqi, 1.5rem);
}
```

The title grows with the card, not with the window. A card in the sidebar stays small. The same title in the main column can go up to `1.5rem` and then stop, because `clamp()` caps it.

See [CSS units](https://flaviocopes.com/css-units/) for how this sits next to `rem`, `em`, and `vw`.

If the browser cannot find a size container, these units fall back to the small viewport units (`svw`, `svh`, and friends). If a type size looks "page sized" instead of "card sized," you forgot `container-type` on the wrapper.

`cqh` and `cqb` only do something useful when the container has `container-type: size`. With `inline-size`, height is not being tracked as a query container.

## Why containment exists

Size queries need a hard number. If the container's width depended on the child's width, and the child's width depended on the query, the browser would loop.

So a size container applies **inline-size containment**. The wrapper's width comes from its parent, not from its children.

That is usually what you want. A grid cell or a sidebar column already has a width.

It bites you when the wrapper was shrink-wrapping its content:

```css
.badge-wrap {
  display: inline-block;
  container-type: inline-size;
}
```

`inline-block` wants to be as wide as its content. Containment says the content cannot define that width. You get a collapsed or surprising box.

The fix is to put containment on an element that already has a definite width: a grid item, a flex item with a set basis, a column, a wrapper with `width: 100%`.

If adding `container-type` changes the layout before you write any `@container` rule, the wrapper was the wrong element.

## Traps I keep hitting

**Querying the container itself.** Said it already. Worth repeating. Style the child.

**Using `size` because it sounds more complete.** It tracks height, and it can flatten auto-height boxes. Start with `inline-size`.

**Copying page breakpoints onto components.** [Page breakpoints](https://flaviocopes.com/css-breakpoints/) like 768px or 1024px describe phones and laptops. A card does not become "tablet sized" at 768px. It becomes two-column when *it* has room for two columns. Measure the card. 400px, 28rem, 20em. Whatever matches the design, not the device chart.

**Forgetting that nested containers steal the query.** Name the container you care about.

**Mixing this up with `:has()`.** `:has()` looks at the DOM: "does this card contain an image?" Container queries look at space: "is this slot 400px wide?" You often want both. A card with an image, in a wide slot, is a different layout from a text-only card in a sidebar.

## How I would use this

On this site I would not sprinkle `container-type` on every div.

The page chrome is a media-query job. The header, the footer, the article column, the point where the blog list stops feeling like a list: those follow the window.

I would use container queries on reusable cards that show up in more than one slot.

A course teaser is the obvious one. On `/courses` it can sit in a wide catalog section. In a sidebar or a "keep learning" block at the end of a post, the same teaser is a narrow stack. I do not want a `teaser--sidebar` modifier that I have to remember to add.

A tool card is the same story. The [tools index](https://flaviocopes.com/tools/) is a grouped list today. If I ever drop the same tool summary into a post footer and into a wide index grid, that component should ask its slot, not the viewport.

I would put `container-type: inline-size` on the grid cell or the aside column, not on the card root. The cell already has a width. The card stays a normal flex or grid child.

I would not use them for a one-off landing section that always spans the article. That is `@media` plus [Flexbox](https://flaviocopes.com/flexbox/). Extra containment for one slot is noise.

The [free CSS course](https://flaviocopes.com/courses/css/) walks through media queries first, then container queries. That order is right. Learn to switch the page. Then learn to switch the component.

## Browser support

Container queries are in every current Chrome, Firefox, and Safari. You do not need a polyfill for a site that already uses Grid and `:has()`.

If you still have to support a very old browser, the fallback is a simpler stacked card, plus a media query for the page. The card will not be as smart in a sidebar. It will still be readable.

My advice: reach for container queries when the same component has to live in two different widths. Keep [media queries](https://flaviocopes.com/css-media-queries/) for the page. Do not make the card pretend it knows how wide the laptop is.
