← All writing
Craft · · 6 min

Container queries

On the question a media query structurally cannot ask, the wrapper element you're going to have to add, and the four things breakpoints still do better.

CSS Responsive

Firefox 110 came out a couple of weeks ago with container queries in it. The other two shipped in the fall. So as of this month the feature that has topped every “what would you add to CSS” list for a decade is available in all three engines, and I’ve been rebuilding a component library with it since.

The framing everywhere is that this kills the breakpoint. It doesn’t, and the way it doesn’t is the interesting part, so I’ll do the good news first and then be pedantic.

The question a media query can’t ask

A media query asks about the viewport. That’s the only thing it can ask about, and for a decade we’ve been using it as a proxy for a question we actually wanted to ask, which is: how much room does this component have?

Those two coincide only for things that span the page. For anything reusable they come apart immediately. A card in the main column at 1200px has 700px of room. The same card in the sidebar has 280px. Same viewport, same component, completely different correct layout, and a media query cannot distinguish them because from its point of view nothing differs.

So we’ve all done the same workaround: a modifier class. .card--compact in the sidebar, .card--wide in the hero, applied in the template by whoever placed it.

Which means the component doesn’t know how to lay itself out. Its context knows, and has to remember to say so, and every place that uses the component has to be told. That’s the thing I was going on about when custom properties arrived in 2016, and it’s the same complaint I made about modifier classes when :has() shipped, and this is the proper fix for the space half of it.

The component didn't know how to lay itself out. Its surroundings knew, and had to remember to say so, every time anybody used it.

The syntax, and the wrapper

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

.card { display: block; }

@container card (min-width: 28em) {
  .card {
    display: grid;
    grid-template-columns: 8rem 1fr;
    gap: 1rem;
  }
}
CSS

Note what’s being queried: .card-area, the parent, not .card itself.

That’s the constraint at the heart of the feature and it’s worth understanding rather than just working around. An element cannot query its own size, because if changing its layout could change its size, and its size determines its layout, you’ve written a loop. So the query targets an ancestor whose size is settled independently, and the styles apply to things inside it.

The practical consequence is that every queryable component needs a wrapper. On new work that’s fine, it’s one element. On an existing site it means touching the markup everywhere the component appears, which is why adoption in a mature codebase is slower than the enthusiasm suggests. I’ve been adding them as I go rather than in a sweep.

inline-size, essentially always

There are two useful values and one of them is a trap.

container-type: inline-size establishes containment on the inline axis only. The container’s width is independent of its contents, its height still grows to fit. This is what you want almost every time.

container-type: size contains both axes, which means the element’s height no longer depends on its content either, so you must set one, or it collapses. There are legitimate uses and I have not yet had one.

If a container mysteriously has no height, that’s what happened.

The units

The other half of the feature, and the bit that isn’t in the headlines.

cqi is one percent of the container’s inline size, with cqw, cqb, cqmin and cqmax alongside it. Which means you can size things relative to the component’s own space rather than the window’s:

fluid.css
.card__title {
  font-size: clamp(1.1rem, 4cqi, 1.8rem);
}
CSS

A heading that’s proportionate to the card it’s in, wherever the card is, with no breakpoints at all. Viewport units have been able to do a version of this for years and it was always slightly wrong, because a component in a narrow column got type sized for a wide window.

I’d use clamp() around it as above rather than a bare cqi, because unbounded fluid type gets silly at both ends.

Why the breakpoint isn’t dead

Now the pedantry, which I think matters because “media queries are over” is going to mislead somebody.

Page layout is genuinely a viewport question. How many columns does the page have, does the navigation collapse, does the sidebar exist at all. Those are decisions about the window, made once, at the top level, and a media query is the correct tool. Container queries answer a different question and don’t replace that one.

And a media query isn’t only about width. This is the part people forget. prefers-reduced-motion, prefers-color-scheme, prefers-contrast, print, orientation, hover and pointer are all media queries, and none of them is about space at all. They’re about the device and the person using it, and there is no container equivalent because a container knows nothing about either.

So the split I’ve settled on: media queries for the page and for the human, container queries for components. Which is a tidier division than we’ve ever had, because until now the component queries were being smuggled through the page mechanism and it never quite fitted.

Properly portable components

The thing I find exciting, and I’m aware “excited about CSS” is a diminishing register, is that components are now properly portable. I can build a card, hand it to somebody, and it works in the main column, in the sidebar, in a three-up grid and in a modal, without a single modifier class and without anybody having to know where they’ve put it.

That’s what “element queries” meant when people started asking for it around 2013, and the standard answer for years was that it was impossible for the loop reason above. It turned out to be possible with one structural concession, which is the wrapper, and I’d have taken that trade a decade ago without hesitating.

There’s a related thing coming that queries a custom property rather than a size, which opens up the “theme this subtree” case in a way I haven’t fully thought through yet. One at a time.

Read similar posts
6 min

display: flex, finally

Flexbox has crossed the line where I can use it on real work, and the surprise isn't the layouts it enables. It's how much of my CSS turns out to have been compensation for not having it.

6 min

Six breakpoints and a prayer

Device breakpoints go stale the moment a new phone comes out. We can do better than yet another list of numbers.