Back to blog
Sep 2026·3 min readAI-generated

CSS Container Queries: A Practical Guide

#css#frontend

Media queries answer one question: how big is the viewport? That's the wrong question for most components, because a card, a sidebar widget, or a product tile doesn't care how wide the browser window is — it cares how much space it has, which depends on where it's dropped in the layout. Container queries answer the question components actually need answered.

The problem with media-query-only responsive design

Say you build a <card> component that should switch from a horizontal to a vertical layout when it gets narrow. With media queries, you're forced to guess the viewport width at which that happens:

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

@media (max-width: 500px) {
  .card {
    flex-direction: column;
  }
}

This works fine if the card always spans nearly the full viewport. But drop that same card into a narrow sidebar on a 1400px-wide desktop screen, and it never gets the vertical layout it actually needs — the viewport is huge, even though the card's available space is tiny. The component's appearance is coupled to something it has no control over: where its parent decides to place it.

Declaring a containment context

Container queries flip the question: instead of "how wide is the viewport," you ask "how wide is my nearest ancestor that opted into being a query container." You opt an element in with container-type:

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

inline-size means "track the container's width" (the usual case for horizontal layout decisions). container-name is optional — it lets you target a specific ancestor container when there's more than one nested container in play, instead of just querying "the nearest one."

Querying against the container

With containment declared, descendants can query that container's size instead of the viewport's:

.card {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

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

Now the same .card component lays out horizontally when it has 400px+ of container width available — whether that's because it's alone on a wide page or because it's one of three columns in a narrower one. The component reacts to its actual space, not a viewport number that may have nothing to do with it.

Container query units

Alongside @container, there's a matching set of length units that resolve against the query container instead of the viewport: cqw, cqh, cqi, cqb, cqmin, cqmax (mirroring vw/vh/vmin/vmax, but container-relative). This is useful for fluid typography that should scale with the component's own box rather than the whole page:

.card-title {
  font-size: clamp(14px, 5cqi, 22px);
}

5cqi is 5% of the container's inline size — so the title scales up as the card gets wider, clamped between 14px and 22px, independent of what the rest of the page's viewport width is doing.

A common gotcha: querying your own container

container-type establishes containment on an element, and @container queries look at the nearest ancestor with that containment applied — an element cannot query itself:

/* This does NOT work as you might expect */
.card {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    /* .card can't query the container-type it declares on itself */
  }
}

The fix is the pattern from the earlier example: put container-type on the parent (.card-grid), and query it from the children (.card). This trips people up because it's the opposite of how you'd naturally reach for it — you want the card to react to its own size, but the containment has to live one level up.

Browser support and fallback

Container queries have solid support across current Chrome, Firefox, Safari, and Edge. For anything that needs to work on genuinely old browsers, @supports lets you provide a media-query fallback:

.card {
  flex-direction: column;
}

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

@supports (container-type: inline-size) {
  @media (min-width: 700px) {
    .card {
      flex-direction: column; /* undo the media-query version */
    }
  }

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

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

Takeaway

Media queries describe the viewport; container queries describe the component's actual available space, which is almost always the thing you meant to ask about in the first place. If you've ever written a component that looks right at the top of the page and wrong the moment it's reused in a sidebar, that's the exact problem container queries exist to fix.

NewerBuilding a Custom Data-Fetching Hook in ReactOlderHow the Virtual DOM Actually Works