⌨️ Web development

CSS container queries: truly responsive components

Container queries adapt a component to its container's space rather than the window: syntax, cqi units and when to prefer them over media queries.

Media queries made the responsive web possible, but they have a structural limit: they respond to the width of the window, not to the width of the space a component sits in. A product card can appear wide on the home page and narrow in a sidebar, on the same screen. With media queries alone you are forced to write specific variants for each position. Container queries solve the problem: the component adapts to its own container, wherever it is placed.

The problem in an example

Imagine a card with an image and some text. When there is room, you want the image on the left and the text on the right; when space is tight, the image on top and the text below. With a media query you would write:

@media (min-width: 800px) {
  .card { display: flex; }
}

But if, with a 1200-pixel window, the card ends up in a 300-pixel side column, it will still be laid out horizontally, squashed and unreadable. The window is wide; the container is not.

Declaring a container

To use container queries you first have to say which element acts as the reference container, using the container-type property:

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

The inline-size value tells the browser to track the size along the inline axis, which is the width in horizontal writing. There is also size, which considers both dimensions, but it requires the container's height not to depend on its content, so in practice inline-size is used almost every time.

Then you write the rule with @container:

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

@container (min-width: 420px) {
  .card {
    grid-template-columns: 160px 1fr;
  }
}

Now the card switches to a horizontal layout only when its container is wider than 420 pixels, regardless of the window width.

An important rule

An element cannot query itself: the query always refers to the nearest ancestor that has been declared a container. That is why a wrapper element is often used (the card-wrapper in the example), or the column or grid cell the component is placed in is declared as the container.

Named containers

When containers are nested, you can give one a name and query that one specifically:

.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

/* equivalent shorthand */
.main {
  container: main / inline-size;
}

@container sidebar (max-width: 300px) {
  .widget-title { font-size: 1rem; }
}

Without a name, the query looks at the nearest container; with a name, it skips the ones in between.

Container-relative units

Along with the queries came new units that refer to the container:

Unit Meaning
cqi 1% of the container's inline size
cqb 1% of the container's block size
cqw / cqh 1% of the container's width / height
cqmin / cqmax the smaller / larger of cqi and cqb

They are very handy for typography that scales with the component:

.card h3 {
  font-size: clamp(1rem, 4cqi, 1.6rem);
}

The heading grows with the width of the card, but stays within a readable minimum and maximum.

Media query or container query?

It is not about replacing one with the other. Each has its own scope:

  • Media queries for decisions that really concern the page and the device: overall layout, number of main columns, user preferences such as prefers-color-scheme or prefers-reduced-motion, orientation, print.
  • Container queries for reusable components: cards, widgets, forms, design-system elements that may end up in different contexts.

A good rule: the window decides the page structure, the component's space decides how the component looks.

Side effects to know about

Declaring container-type: inline-size turns on containment of the inline size: the container can no longer derive its width from its children. This is usually not a problem for blocks that take up all the available width, but an element that should be "as wide as its content", such as an inline-block element or a flex item with no defined size, can collapse to zero width. If something disappears after you add container-type, check this first.

Also, apply the property with judgement: declaring every element on the page a container is pointless and makes it harder to tell which container is answering a query.

Support and strategy

Size-based container queries are supported by all major up-to-date browsers. If you need an acceptable result even on very old browsers, write the base style for the smallest space (stacked, single column) and use @container to enhance it: browsers without support will still show a working version.

There are also style queries, which query the value of a custom property on the container, for example @container style(--variant: compact). Support is less consistent, so it is best to treat them as a progressive enhancement.

In short

  • Container queries adapt a component to the space of its container, not to the window.
  • You declare the container with container-type: inline-size and write the rule with @container.
  • cqi and related units allow typography and spacing proportional to the component.
  • Media queries remain the right tool for page layout and user preferences.
  • Containment can collapse elements with no defined width: check for it straight away.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also