This content originally appeared on dbushell.com and was authored by dbushell.com
CSS container queries are spectacular!
And I keep being mislead by the same mistake. It’s starting to irritate me! In this blog post I document a bad code pattern to avoid.
The Mistake
Consider this example with a <div>
and two <p>
elements inside:
<div class="box">
<p class="query-less">Less than 600px</p>
<p class="query-greater">Greater or equal to 600px</p>
</div>
With the CSS below I’ve made box
a named container. The two child elements are hidden by default.
.box {
container: box / inline-size;
}
[class^='query'] {
display: none;
}
A container query can show the first paragraph using “less than”:
@container box (inline-size < 600px) {
.query-less {
display: block;
}
}
And the second paragraph is shown with a “greater than” query:
@container box (inline-size >= 600px) {
.query-greater {
display: block;
}
}
Simple, right? The box
content changes depending on its inline size. Basic container query stuff and perfect for responsive components.
Now consider the following container query change:
.box {
container: box / inline-size;
}
[class^='query'] {
display: none;
}
@container (inline-size >= 600px) {
.box {
display: none;
& .query-greater {
display: block;
}
}
}
Note that I’m using standard CSS nesting and I’ve removed the box
name from the @container
query.
Assuming the query matches, how are styles applied?
- Is
.box
now hidden? - Is
.query-greater
now visible?
The answer is “no” the box remains visible, not hidden, and “yes” the second paragraph is now visible.
Does that look weird to you? It looks weird to me! The parent style is not applied but the nested style is applied. When I think about it logically it makes sense but it’s not obvious to see. The presentation is deceiving. With nested CSS one would assume all previous selectors match — and technically they do — but not all styles are applied here.
There are a few lessons to remember:
- The container itself can’t be styled inside a
@container
query - If the container name is omitted the closest parent container is used
In my example code, because .box
has no parent container display: none
is never applied. The .box .query-greater
selector targets a child of .box
so this is applied based on the box
container size. When “un-nested” the selectors and their styles are entirely independent.
CodePen Example
I’ve published a full CodePen demo to show my bad code pattern.
In Practice
I keep falling into the trap of the failed pattern:
@container (inline-size >= 600px) {
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
/* Card children styles */
& > img {
border-radius: 15px;
}
}
}
I have a .card
component with an optional image. My intention is to responsively adapt the component as a two-column grid if an image exists. This fails because card
is the container and so grid styles won’t apply. However, the card children are styled, which makes me very confused until I realise I’ve made the same mistake, again.
So here I am blogging my mistake in the hopes of committing it to memory.
In such cases I should always specify the container name in the query — i.e. @container box
— because that’s always my intended target. This would help visually highlight the mistake of trying to style the container.
Ahmad Shadeed has a guide including more “Common pitfalls for container queries”. Well worth reading the whole guide.
This content originally appeared on dbushell.com and was authored by dbushell.com