Container queries

Let a component respond to the space it is given rather than to the size of the window.

16of 26
≈ 10Min read
8Parts

The problem with media queries

A media query asks about the viewport. But a card does not care how wide the window is — it cares how wide it is. The same card might be 900px wide in a hero, 400px in a two-column grid and 240px in a sidebar, all on the same screen.

With media queries you end up writing .sidebar .card and .hero .card overrides, and the component stops being portable: it only works in the places you thought of.

Declaring a container

  • inline-size — queries about width. This is what you want almost always.
  • size — width and height, and it requires the container to have a definite size in both directions.
  • normal — no size queries; only style queries work.
two steps
.card-wrap {
  container-type: inline-size;
  container-name: card;
}

/* shorthand for both */
.card-wrap {
  container: card / inline-size;
}
A container queries itself, not its own box

The rules inside @container apply to the descendants of the container, never to the container element itself. So you need a wrapper: the element you query and the element you style cannot be the same one.

Querying it

Drop that card into a sidebar and it stacks. Drop the same markup into a wide hero and it goes two-column. Nothing outside the component had to be told.

the card decides its own layout
.card {
  display: grid;
  gap: 0.75rem;
}

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

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

  .card h3 { font-size: 1.6rem; }
}

Without a name

Leaving the name out queries the nearest ancestor that is a container. That is fine for a simple case; name them as soon as there is more than one level, or you will be debugging which container answered.

the nearest ancestor container wins
.wrap { container-type: inline-size; }

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

Container query units

This is the piece media queries could never do: a heading that is large in a wide card and small in a narrow one, on the same screen, with one rule.

type that scales with the component, not the window
.card h3 {
  font-size: clamp(1.1rem, 0.9rem + 2cqi, 2rem);
}
UnitRelative to
cqw1% of the container's width
cqh1% of its height
cqi1% of its inline size — usually the width
cqb1% of its block size
cqmin / cqmaxThe smaller / larger of cqi and cqb

Style queries

A style query asks about the value of a custom property on the container. It lets a variant set one token and have descendants respond, without every descendant needing a modifier class. Support is newer than size queries — check before relying on it.

querying a custom property
.panel { container-name: panel; }

@container panel style(--tone: warning) {
  .panel-title { color: #a8471f; }
}

What it costs

  • container-type: inline-size applies contain: layout inline-size style — the element becomes a containment boundary. Its children can no longer affect its width.
  • That means a container cannot be sized by its contents in the inline direction. A container-type: size element needs a definite height too, or it collapses.
  • Do not put container-type on body or on every div. Put it on the specific wrapper whose width the component should follow.

Using both

Media queries and container queries answer different questions, and a real stylesheet uses both:

  • Media queries for the page: how many columns does the layout have, is the navigation a bar or a drawer, is this print, does the reader want reduced motion.
  • Container queries for components: how does this card, this media object, this table arrange itself in the space it was given.

Practice