CSS Container Queries

By

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.

~~~

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 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.

.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:

.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.

.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:

@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:

.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:

<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>
.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 for the page and 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.

<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>
.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:

.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:

@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.

UnitMeaning
cqw1% of the container width
cqh1% of the container height
cqi1% of the container inline size
cqb1% of the container block size
cqminthe smaller of cqi and cqb
cqmaxthe larger of cqi and cqb

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

.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 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:

.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 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 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. Extra containment for one slot is noise.

The free CSS course 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 for the page. Do not make the card pretend it knows how wide the laptop is.

Tagged: CSS · All topics
~~~

Related posts about css: