I recently published a detailed article on the difference between media queries and container queries.
I used that article to make the case for a container-query-first workflow, but I also listed two “gotchas” of container queries that make them slightly annoying.
By far, the biggest “gotcha” is the fact that container queries need their direct parent manually declared as a “container” using the container-type
property.
From the article:
For container queries to work, the browser needs to calculate the size of the containing element when a container query is used.
Apparently the CSS team felt this was going to be too resource-heavy to do at all times for all “containers,” so they created a new property called
container-type
that must be manually declared on the parent of the element that uses container queries.As you can probably guess, I hate this. I hate it more than it’s possible to hate it. Why? Because it creates tons of opportunities for container queries to fail.
If you place a card in some new area of the layout and forget to declare that area as a container, your container queries won’t work.
Yeah, that sucks. But, instead of moping around complaining about the spec, I set out to find a workable solution.
Programmatic Container Queries
The goal of using container queries is simple: It shouldn’t matter where I put an element, it should check its available space and change its configuration accordingly.
In order to do this in some sort of automatic or programmatic fashion, we need a way for the Block itself to tell its parent to be a measurable container.
Turns out, we can do exactly that!
By combining the :has
selector, CSS nesting, and an inversion of the &
selector, we arrive at the solution, which I call “The Magical ‘Has Me’ Selector” [cue dramatic music].
All-in-all, it’s an insanely simple and practical selector:
.card {
:has(> &) {
container-type: inline-size;
}
}
Let’s break it down, because it’s going to confuse a lot of people at first glance.
Inverting the &
selector
The &
selector is what I refer to as the “me” selector. In CSS nesting, it’s a placeholder token for the current selector.
It’s commonly used to add pseudo elements, for example:
.card {
&::before {
content: 'I'm a pseudo element';
}
}
The above code would read as, “add a pseudo element to me (.card
)” and it’s essentially the same as writing .card::before
.
Well, guess what? You can uno reverse it!
Look at this example:
.card {
.hero-section & {
border: 5px solid red;
}
}
This is the same as writing:
.hero-section .card {
border: 5px solid red;
}
Which is amazing, because it essentially lets you write globalized CSS from the confines of CSS that’s nested in an element. It’s a “get out of jail free” card that has major benefits.
For container queries, this means we can target parents from within the children.
.card {
:has(> &) {
container-type: inline-size;
}
}
The “has me” selector is the same as opening a global stylesheet and writing:
:has(> .card) {
container-type: inline-size;
}
Except, I don’t have to manually keep that :has() list updated. It keeps itself up-to-date because it’s written dynamically from every independent Block that uses/requires container queries.
“Direct Parent of an Unidentified Object” Selector
Anyone familiar with CSS knows that >
is known as the “direct child” selector.
And for a long time, CSS didn’t have a “parent” selector at all. That’s what :has
gave us when it arrived on the scene.
Well, it turns out the nested Has Me selector is the gift that keeps on giving, because it creates the option of not only having a “Direct Parent” selector, but a “Direct Parent of an Unidentified Object” selector.
It’s easy to write:
:has(> .card) {
container-type: inline-size;
}
But in order to do that, you need to know that you want to target the .card
selector; and you have to manually update the list every time a new container query element is added; and in page builders that allow for class name editing, you want to be able to change the class names without breaking the CSS; and you potentially want to have patterns that are shareable, which means the CSS must travel with them instead of existing outside of them.
What we really need is a way to select the direct parent of something, without specifically naming that something.
:has(> &)
is Him, as the kids say.
Oh, and remember, there’s a reason the official CSS spec doesn’t make every box a container-type, so we should avoid doing that as well. Using :has
without a direct child selector makes almost everything a container-type
, which is a violation of the spec in my opinion.
Conclusion
The Has Me selector is a perfect solution for the main “gotcha” of container queries, but it also comes in very handy in other scenarios, especially scenarios where you need to select the direct parent of an unnamed element (the element is named dynamically via the &
selector).
Try it out, have fun, share it with others, and let me know what you think! You can always tag me on X (@thekevingeary) if you have questions or want to pass along credit. Just make sure you always introduce the concept with dramatic music.