CSS functions like :is(), :not() and :has() are powerful tools that make it much easier to select elements specifically. We've written before about how ::where() :is() and :has() make your life easier, as well as how they handle specificity.
When you combine CSS Functions, the nesting of these functions matters. In this post, we'll look at the difference between :has(:not)
and :not(:has)
, and how to approach decoding CSS selectors that use these nested CSS functions.
Our HTML structure
In order to figure out what a given CSS selector does, it's easier if we have a simple HTML structure to work with. Let's use the following structures:
<!-- card 1 -->
<div class="card">
<img />
<span />
</div>
<!-- card 2 -->
<div class="card">
<span />
</div>
You've probably written this kind of code before: There's a container div
and inside it an optional image and a span
containing some text.
Using CSS, we want to select a card when it does not have an image. For this, we're going to look at the difference between these two CSS selectors and see how they select, or don't select, either of the .card
elements.
.card:has(:not(img)) {}
.card:not(:has(img)) {}
Both of these CSS selectors are selecting the element with a class of card
, and both have a specificity of (0,1,1) but they're adding different conditional logic.
When you look at the above selectors, there are three things that can help improve he understanding of what they do:
- Add the implicit selectors that have been left out
- Break down the selectors into parts
- Work from the inside out
Adding implicit selectors
When you have a CSS function or pseudo-selector (both start with a single :
), they're expected to be attached to another selector:
a:focus
div:is(.active)
button:active
- ...etc
If you don't attach them directly to a selector, the CSS parsing logic adds an implicit universal selector (*
) in front of the CSS function or pseudo-selector:
a :focus
=>a *:focus
div :is(.active)
=>div *:is(.active)
button :active
=>button *:active
You can see how the pseudo-selectors are now applied to the children of the element, and not the element itself.
If you use :has()
, then the selector you put inside of it is already applied to the child elements of the element you're selecting. This is different from :is()
and :not()
, which are applied to the element itself.
That means that when we add implicit selectors, we can add the implicit universal selector in front of the nested :not()
selector, and repeat the selector for the nested :has()
selector:
.card:has(*:not(img)) {}
.card:not(.card:has(img)) {}
Breaking down the selectors into parts
When you break down the selector into its parts, it's easier to see what each part does. Let's break down the first selector by extracting the part inside the CSS function:
.card:has() {} /* Select a .card that contains some other selectors */
*:not(img) {} /* select any element that is not an image */
/* and */
.card:not() {} /* Select a card if some other selectors aren't matched */
.card:has(img) {} /* Select a card that contains an image */
Now we can look the separate parts and see what they do.
Working from the inside out
Eventually we're trying to select the .card
, so when looking at the selector inside our CSS function, we can simplify our HTML to the contents of the .card
:
<!-- first card -->
<img />
<span />
<!-- second card -->
<span />
Now, we'll take a look at the selectors and compare them to our simplified HTML:
The first nested selector: *:not(img)
img
is an image so*:not(img)
does not matchspan
is not an image, so*:not(img)
does match
Since the span is in our HTML structure for both cards, we can simplify and rewrite *:not(img)
to span
.
The full first selector now becomes .card:has(span)
: select any card that contains a span
element. Both HTML examples are a card that contain a span
element, so both cards get selected.
The second nested selector: .card:has(img)
- The first card contains an image, so
.card:has(img)
does match - The second card has no image, so
.card:has(img)
does not match
In other words, if you look at that :has()
we get a "true" for the first card and for the second card we get "false".
If we then put that back into the full selector we get .card:not(true)
and .card:not(false)
.
:not()
inverts the boolean values, so out of the two HTML cards, the first is not selected and the second is, because it doesn't have an image.
Conclusion
When you're trying to figure out what a CSS selector does, it can be helpful to break it down into its parts and work from the inside out. This makes it easier to see what HTML each part of the selector selects, and you can then recombine them find the full HTML that the selector matches against.
In this case, we looked at the difference between :has(:not)
and :not(:has)
, and how they select elements based on the presence or absence of other elements.
If the above explanation didn't work for you, then we highly recommend taking a look at Manuel's exploration of the same: :has(:not()) vs. :not(:has()).