CSS has a lot of ways to select elements in HTML and so many options can make it difficult to choose which method is most sufficient. This becomes very apparent when we have multiple elements on the pages that share styles but one of them, in particular, has a different style than the rest.
The traditional way to solve this problem is to place another CSS class on the component that is going to get this Special style. It may look something like this
<div class="card">...Some content goes here</div>
<div class="card special-card">
...Some content goes here
<p class="card__subTitle">Some more content for the special class</p>
</div>
<div class="card">...Some content goes here</div>
The middle div
element has two CSS classes: 1) one denoting that it will get all the styles in the card class and 2) denoting that it will get all the styles from the special-card class.
There hasn't really been a way to select an element with CSS if it has certain descendants -- at least not until recently.
The HAS Selector
The :has
selector also comically known as the parent selector is a pseudo-class that was introduced in the CSS spec. :has
allows for the selection of an element if the arguments passed into :has
match at least one element when anchored against the parent element. Essentially it allows for the selection of an element based on the presence or absence of descendant elements.
Revisiting our example above, let's say our special card had a paragraph that implemend its own padding in the in-line direction. Since we want all the cards to have the same amount of padding we need some way of addressing this problem. We can reach for the solution above, or we can implement the :has
selector. We can select the card if it has a paragraph as its child and then set the in-line padding on the card to 0. It looks something like this:
.card:has(.card__subTitle) {
padding-inline: 0;
}
The above code block translates to if an element with the class of .card
has a descendant with the class of .card__subTitle
then set the in-line padding on the parent element (in this case the element with the class of .card
) to 0.
<div class="card">...Some content goes here</div>
<div class="card special-card">
...Some content goes here
<p class="Title">Some more content for the special class</p>
</div>
<div class="card">...Some content goes here</div>
Multiple selectors inside :has
The cool thing is that we aren't just limited to putting one item inside the :has
pseudo-class. We can chain on selectors and get really specific as to when we want this parent element
to be selected. Chaining on selectors looks something like this:
.card:has(.card__subTitle, .Title) {
padding-inline: 0;
}
The above code block translates to if an element with the class of .card
has a descendant with the class of .card__subTitle
or .Title
then set the in-line padding on the parent element (in this case the element with the class of .card
) to 0.
You might have noticed the keyword OR above. This is important and a key differentiator between :has
and other selectors. Usually with CSS if you are chaining selectors, if one of the selectors in your chain does not match, then the entire rule is skipped. This is not the case with the :has
pseudo-class, and it is something that differentiates it from other selectors. As long as there is one matching selector inside the :has
pseudo-class, then the parent element will be selected.
You might be wondering how we may go about saying AND with this selector. More specifically how do we go about saying the following: Select the element with the .card
class if its descendants have elements with the .first
and .second
classes. Using the scenario above .card:has(.first .second)
matches at least one. What happens if we require both to be present?
We can attach another :has
pseudo-class. It would look something like this:
.card:has(.first):has(.second) {
padding-inline: 0;
}
The above code block translates to if an element with the class of .card
has a descendant with the class of .first
and .second
then set the in-line padding on the parent element (in this case the element with the class of .card
) to 0.
The other cool thing about :has
is that we can chain complex selectors when the parent element
is to be selected. We can say something like this:
.card:has(> .first .title) {
/* Some css rule */
}
Only select an element with a .card
class if it has direct decedents with a class of .first
which has a descendent with a class of .title
.
More than just a parent Selector
The :has
pseudo-class is more than just a parent selector because we can use it to select other elements that are in relation to the parent element. The following describes how we are using the :has
pseudo-class to select a paragraph element.
.card:has(.subTitle) > p {
/*Some Css Rule */
}
The above code block translates to: select any paragraph element that is a direct decedent of .card
if the .card
has a descendant element with the .subTitle
class. Putting this into practice, below we can see that the manager's name is in bold while the staff's name is not. This is being done with a rule similar to the one we described above.
Specificity
Calculating specificity with the :has
pseudo-class gets a bit complicated and so we are going to walk through an explanation and then some examples.
Let's say we have the following scenario:
.card:has(.first p) {
/* Some css rule */
}
What is the specificity here? We first look at the selector outside the :has
pseudo-class and then focus on the selectors inside the :has
pseudo-class. Here we have 1 class
selector outside :has
and then 1 class selector
& 1 element selector
inside :has
. Therefore, we say that the specificity of the entire selector is 2 class selectors
& 1 element Selector
.
The interesting thing is that calculating specificity changes when there are multiple selectors inside the :has
pseudo-class that are separated by a comma. In this case :has
will only use the specificity of the most specific selector. This is demonstrated below:
.card:has(.first, p) {
/* Some css rule */
}
What is the specificity here? Remember, we first look at the selector outside the :has
pseudo-class and then focus on the selectors inside the :has
pseudo-class. Here we have 1 class
selector outside :has
& 1 class selector
& 1 element selector
inside :has
. Since the class selector and the ** element selector** are separated by a comma, we look at the selector with the highest specificity. In this case, it would be the class selector. Therefore, we say that the overall specificity is 2 class selectors
.
In the case where we have multiple :has
pseudo-classes, we calculate the specificity for the individual :has
and then combine them all to get an overall specificity. The following is an example of this:
.card:has(.first):has(.second p) {
padding-inline: 0;
}
Here we have 1 class
selector outside :has
, 1 class selector
inside the first :has
, and 1 class selector
& 1 element selector
inside the second :has
. These specificities are all combined to give us an overall specificity of 3 classes and 1 element.
Browser support
According to Can I use as of the time of writing this article the :has
pseudo-class is at 76% support in terms of browsers. Moreover, it is supported in the latest version of Chromium & Safari, and it is under an experimental flag in firefox (thus I expect it to be fully supported in firefox fairly soon).
Conclusion
This :has
selector is an incredible selector and it gives us another way to author the CSS for our applications. I can't wait for it to be fully supported across all major browsers!
Alight I'll catch you in the next one -> peace out!
Top comments (0)