RizTech Academy logo
RizTech Academy
Responsive DesignLesson 5 of 730 min

Container queries, and what they fixed

The previous lessons all shared one limitation: a media query knows the size of the window, and a component does not care about the window. It cares how much space it has.

Put the same card in a 300px sidebar and in a 900px main column. The viewport is identical for both, so a media query cannot tell them apart. For twenty years the workarounds were a modifier class you had to remember to apply, or JavaScript measuring elements and adding classes on resize.

Container queries fix it properly, and they are the biggest change to CSS layout since Grid.

The two parts

.card-area {
  container-type: inline-size;     /* 1. declare a container */
}

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

@container (width >= 30rem) {      /* 2. query it */
  .card {
    grid-template-columns: 12rem 1fr;
    align-items: start;
  }
}

Now the card becomes two-column when its own container is at least 30rem wide — in the main column but not in the sidebar, at the same viewport width. Drop the component anywhere and it adapts to where it landed.

You must declare container-type on an ancestor, or @container matches nothing. That is the commonest reason it appears not to work, and it fails silently as always.

container-type values

container-type: inline-size;   /* query the inline size (width in English) */
container-type: size;          /* query both axes */
container-type: normal;        /* not a size container — only style queries */

Use inline-size almost always. It is the one that is safe, because of the next section.

The containment cost

Declaring a size container applies containment on that axis, and this is the trap worth understanding before you use it.

container-type: size — querying both axes — means the container can no longer be sized by its children's height. Its height collapses unless you give it one. That is inherent: the children's styles depend on the height, and the height cannot depend on the children, or it is circular.

inline-size only contains the inline axis, and blocks already get their width from their parent rather than their content, so nothing breaks. This is why inline-size is the default choice and size is a special case you reach for knowingly, for something with a fixed height.

There is a second consequence either way: a size container cannot be queried by itself. @container inside .card queries .card's container, not .card. So the container and the thing being styled must be different elements — hence the .card-area wrapper above. Trying to put container-type on the card and query the card is the second commonest mistake.

Named containers

With nested containers, say which one you mean:

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

@container card (width >= 30rem) { .card { … } }
@container sidebar (width >= 20rem) { .card__meta { … } }

The shorthand:

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

An unnamed @container query matches the nearest ancestor container, which is usually right and occasionally surprising. Name them once you have more than one.

Container query units

Units relative to the container rather than the viewport:

.card__title {
  font-size: clamp(1rem, 5cqi, 1.5rem);
  padding: 2cqi;
}
Unit Relative to
cqi the container's inline size
cqb the container's block size
cqw / cqh its width / height
cqmin / cqmax the smaller / larger of the two

cqi is the one to use. It is vw for components: a title that scales with its card rather than with the window, so the card in the sidebar has proportionally smaller text without a single query.

The same accessibility rule from the fluid-type lesson applies — keep a rem term in the middle of the clamp(), because cqi alone ignores the visitor's font-size setting just as vw does.

When to use which

Not a replacement for media queries. They answer different questions:

Question Tool
How big is the window? media query
How big is this component's space? container query
Is the whole page layout changing? media query
Is one component adapting to its slot? container query
What does the user prefer? media query
Is it a phone or a mouse? media query

Page skeleton: media query. Components inside it: container query. That division holds up well in practice, and it means a component's CSS no longer contains any knowledge of the page's breakpoints — which is the real win. A card that works in the sidebar, the main column, a modal and a footer, with no variants, is a component you can move without thinking.

A real example

A product card that is stacked when narrow and side-by-side when it has room, used in two places at once:

/* the page: media query */
.layout {
  display: grid;
  gap: 2rem;
}
@media (width >= 60rem) {
  .layout { grid-template-columns: 1fr 18rem; }
}

/* both slots are containers */
.layout > * { container-type: inline-size; }

/* the component: container query, no page knowledge at all */
.product {
  display: grid;
  gap: 0.75rem;
}
.product img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
.product h3 { font-size: clamp(1rem, 0.9rem + 2cqi, 1.375rem); }

@container (width >= 26rem) {
  .product {
    grid-template-columns: 10rem 1fr;
    align-items: start;
  }
  .product img { aspect-ratio: 1; }
}

At a 1200px viewport the main column is wide, so those products are side-by-side; the 18rem sidebar is narrow, so the same markup is stacked. One component, no variants, and nothing in .product refers to 60rem.

Style queries

The other half of @container, and newer:

.card { container-type: normal; }

@container style(--variant: featured) {
  .card__title { font-size: 1.5rem; }
}

Queries a custom property value on the container rather than its size. Useful for theming a component from a parent without adding classes at every level. Support is still arriving for the general case — check caniuse.com — so treat this as worth knowing rather than worth relying on.

Support and fallback

Container queries are supported in all current browsers. For an older one, the sensible fallback is that the query simply does not match, so you get the base styles — which, if you have written mobile-first, is the stacked single-column version. That degrades acceptably with no effort.

If you need to be explicit:

@supports (container-type: inline-size) {
  /* the enhanced version */
}

Check your work

What a media query cannot know. How much space a component has.

The two parts. container-type on an ancestor, then @container.

The commonest reason it does nothing. No container-type declared.

Why inline-size rather than size. size contains both axes, so the container's height collapses; blocks already take their width from the parent, so inline-size breaks nothing.

Why the container must be a different element. A container cannot be queried by itself, so a wrapper is required.

What an unnamed @container matches. The nearest ancestor container.

What cqi is. vw for components — relative to the container's inline size.

The accessibility rule that carries over. Keep a rem term in a clamp(); cqi ignores the font-size setting just as vw does.

The division of labour. Page skeleton by media query, components by container query.

The real win. A component's CSS contains no knowledge of the page's breakpoints.

What a style query queries. A custom property value on the container.

What the fallback is in an old browser. The query does not match, so you get the mobile-first base styles.

Practice

  1. Put the same card in a 300px sidebar and a 900px main column and try to style them differently with a media query. Confirm you cannot.
  2. Add container-type: inline-size to both slots and a @container query. Confirm they now differ at one viewport width.
  3. Remove the container-type and confirm the query silently stops matching.
  4. Put container-type on the card itself and query the card. Explain what happens.
  5. Set container-type: size on a container whose height comes from its content. Measure the height before and after.
  6. Nest two containers and use an unnamed query. Work out which one it matched.
  7. Name them both and target the outer one deliberately.
  8. Use 5cqi for a title's font size and resize the container, not the window.
  9. Compare 5cqi with 5vw for the same card in the sidebar.
  10. Set the browser's default font size to 24px and check whether your cqi title responded. Then add a rem term.
  11. Build the product-card example and confirm one component serves both slots with no variant class.
  12. Search your own CSS for a .card--narrow-style modifier and replace it with a container query.
  13. Wrap an enhancement in @supports (container-type: inline-size).
  14. Look up container query support on caniuse.com and note the oldest version that has it.

Official documentation

Next: honouring what the visitor has already told their device.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship