They moved the card themselves
Container queries are in all three engines as of this month, and the interesting part is the constraint they couldn't get rid of.
A screenshot came in from a client with the “Add to cart” button hanging off the right edge of a card, and a question about whether they’d done something wrong.
They hadn’t. They’d built a landing page in the page builder, decided the featured product looked better beside the copy than above it, and dragged the block into the sidebar column. The page builder let them, because letting them is the entire reason it’s installed.
That card is a horizontal one. An 8rem image on the left, title and price and button stacked alongside it. In the main column it’s a comfortable arrangement, and in a 280px sidebar it leaves about 100px for the text, so the title came apart onto 3 lines and the button, which has a min-width on it, went straight through the wall.
There is a .card--stacked on that component that lays it out vertically and looks perfectly nice at 280px. It gets applied in the template, by a developer. There wasn’t one in the room.
The window can’t see the sidebar
A media query asks about the viewport. That’s the only thing it can ask about, and for about a decade every one of us has been using it as a stand-in for the question we actually wanted answered, which is how much room this particular component has.
Those two coincide for anything that spans the page, which is why the arrangement held together as long as it did. For anything reusable they come apart the moment you reuse it. That card in the main column at a 1200px viewport has 700px to work with. The same card in the sidebar has 280px. Same viewport, same markup, two completely different correct answers, and a media query cannot tell them apart because from where it’s standing nothing differs.
So we all did the obvious thing and answered the question in the template instead. .card--stacked in the sidebar, .card--wide in the hero, applied by whoever placed the component.
Which is fine while whoever places the component is me. It stops being fine the moment it’s a person in a CMS who has never heard of a modifier class and has no reason to. I’ve been here before, with the hero image that grew to 2.4 MB 4 months after I launched the site. The system worked exactly as designed, and the design assumed a developer.
I made almost this exact complaint when :has() turned up in one browser last spring, that a good share of the modifier classes in any project exist because CSS couldn’t ask a question and somebody had to answer it in the template. That one was about content. This one is about space, and space is the more common of the two by a distance.
Chicken, egg, and one more div
Firefox 110 came out a couple of weeks ago with container queries in it. Chrome and Safari have had them since the end of last summer, so as of this month the thing that has topped every “what would you add to CSS” thread since about 2013 works everywhere I care about. There’s no equivalent sentence for email, where I spent January explaining to a client that the set of things you can count on only ever gets smaller, and nobody has the standing to decide otherwise.
.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;
}
}
Note what’s being queried up there. .card-area, the parent. Not the card.
That’s the constraint the whole feature is built around. It’s worth understanding instead of just routing around it. An element cannot query its own size, because if the query decides the layout, and the layout decides the size, and the size decides the query, then you’ve written a loop and handed the browser the job of settling it. So you query an ancestor whose size was already decided by something else, and the styles land on what’s inside it.
Which means every component you want to query needs an element wrapped around it. On new work that costs nothing, it’s one div and you were going to have one anyway. On a site that already exists it’s a markup change in every template the component turns up in, and that, not enthusiasm, is what actually sets the pace of adoption. I’ve been adding them as I touch things instead of doing a sweep. I’m maybe a third of the way through that component library.
You can write it as container: card / inline-size in one line if you’d rather, which is what I do. The long form is up there because it’s easier to read when you’ve never seen either.
Where the height went
Two values, and one of them is a trap.
container-type: inline-size establishes containment on the inline axis only. The container’s width stops depending on its contents and its height still grows to fit whatever is in it. This is the one you want essentially every time.
container-type: size contains both axes, which means the height stops depending on the contents too, so if you haven’t set one the element collapses to nothing. There are legitimate uses for that and I have not had one yet.
If a container has mysteriously become 0 tall, that’s what happened, and the same surprise has a quieter form on the inline axis. An element that was sizing itself to its contents, a float or an inline-block or a grid item on an auto track, stops doing that the moment you make it a container. Nothing collapses. It just gets wider than you meant. It looks nothing whatsoever like the line of CSS you added 🥲
A percentage of the thing it’s in
The other half of the feature, and the half nobody put in a headline.
cqi is 1% of the container’s inline size, with cqw, cqb, cqmin and cqmax alongside it. Which means you can size things against the component’s own room instead of the window’s.
.card__title {
font-size: clamp(1.1rem, 4cqi, 1.8rem);
}
A heading proportionate to the card it’s in, wherever the card turns out to be, with no breakpoint anywhere in it. Viewport units could do a version of this and it was always slightly off, because a component in a narrow column got its type sized for a wide window, which is the same mistake in a smaller font.
I’d keep the clamp() around it. Unbounded fluid type is charming in a demo and gets ridiculous at both ends of a real page.
The breakpoint is fine, thanks
The framing everywhere this month is that this kills the media query. I think that’s going to mislead somebody, so let me be the pedant for a minute.
Page layout is genuinely a viewport question. How many columns the page has, whether the nav collapses, whether the sidebar exists at all. Those are decisions about the window, made once, at the top level, and a media query is the right tool for every one of them. Dragging the window until the layout looks bad is still how I find those numbers and nothing about that changed this month.
And a media query was never only about width, which is the part that gets forgotten in a sentence like “media queries are over.” 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 holding it, and a container knows nothing about either. So there’s no container equivalent for any of them and there isn’t going to be. The parallax hero that gave a coworker a headache came out entirely, and the fade that replaced it sits inside a media query, which is the only place it could have sat.
So the split I’ve landed on is media queries for the page and for the person, container queries for the component. Which is a tidier division than we’ve ever had, honestly, because until now the component questions were being smuggled through the page mechanism and it never quite fit.
The audit I only did half of
The fix on that card was a wrapper and one @container block, and the same component is now correct in the main column, in the sidebar, in the three-up grid on the category page and in the quick-view modal, without anybody having to know where they put it. I’m aware that being excited about a layout feature is a diminishing register at this point in my career. I’ve wanted this since roughly 2013 and the standard answer for most of those years was that it couldn’t be done, for the loop reason above, delivered with the same confidence as the answer about :has(). It turned out to be possible for one structural concession and the concession is a div. I’d have taken that trade a decade ago without asking a follow-up question!
I keep turning over the exercise I set myself last spring and then only did half of. The idea was to go through a project and notice which modifier classes exist purely because CSS couldn’t see the content, so that when the feature is everywhere you already know which things get simpler. I did that. I have the list. And it did not once occur to me to make the other list, the one where the class exists because CSS couldn’t see the space, even though on this codebase that list is longer.
So I’m going back through the library with a slightly different question about each modifier, which is whether it describes the component or describes where somebody put it. The second kind was never really part of the component’s API. It’s a note from the template saying “I know something you don’t,” and it’s been sitting in these files long enough that it reads as design intent.
I didn’t put in my reply to that client that they found this faster than my audit did. They weren’t looking for it. They dragged a block into a column because the page builder said they could, the component came apart, and the reason it came apart is that a developer’s memory was holding it together and no developer was involved. Everywhere else I’m still relying on that, somebody is one drag away from finding it for me.