Practical usage of CSS pseudo-class selectors is(), where(), has() and not()

If you like me, you might come to get a quick overview to solve your specific problem, so here is a quick summary.

:is() and :where() are the same apart of one detail, their specificity is different. The :where() has a specificity value of 0 and :is() would take the most specific argument.

Both selectors can simplify your code. Let's say you want to highlight the first paragraph following any header title, but only if it is a paragraph and if it is following the header title.

1
h1 + p,
2
h2 + p,
3
h3 + p,
4
h4 + p,
5
h5 + p,
6
h6 + p {
7
    color: lime;
8
}
9

Here is an example of the same code using :is() selector.

1
:is(h1, h2, h3, h4, h5, h6) + p {
2
    color: lime;
3
}

Here is how I would read those rules, starting from right to left: "Any P tag followed directly after any of H1..H6 tags will have color: lime".

Below is an example of a fancy way to target HTML elements which follow other elements

Fun fact: Apparently the original name for :is() was :matches(), which may give a hint as to what this selector does.

To summarise, the main benefit is that we can significantly improve the code by removing repetitive and long lists of selectors. Chaining and combining :is(), :where() and :not() we can select specific elements and reduce CSS boilerplate.

:has() and :not() selectors

Firstly, :has() can select a parent or previous sibling element which is simply amazing. It does this by taking a selector list as an argument.

The specificity for :has() is the same as per :is() and :not()

Usage is similar to :is()selector. Here is an example which adds special styling for paragraph if it has an img tag.

1
p:has(img) {
2
    outline: 1px solid pink;
3
    padding: 1rem;
4
}

Simply put, this selector can select any parent element by the argument you pass into it.
For example, you can select any div only if it has img tag.

Also, you can combine it with :is() and make powerful oneliners.

Below is an example where we select h1-h4 tags, and if the next direct sibling is from the has() list, it would receive a red colour.

1

2
:is(h1, h2, h3, h4):has(+ :is(h2, h3, h4, h5)) {
3
    color: red;
4
}
5

Try to create this without :has().

Logical operations

Who said that CSS is not a programming language?

Let's say you want to style the div tag in special way, but only when there are em and code tags. Not sure why you would do that, but for the sake of example, this is how you do it.

1

2
/** Only when both tags em AND code are in the div tag */
3
div:has(em):has(code) {
4
    outline: 1px solid red;
5
}
6

7
/** When one of the tags em OR code are in the div tag */
8
div:has(em, code) {
9
    outline: 1px solid red;
10
}
11
    

To iterate, the first example is when both of the tags are available (AND), and the second is when any of the tags are available (OR)

Could we have self-aware boxes which would change the design based on the content? Oh, but we can! Take a look at the CSS example:

1

2
figure:has(.theme:checked) {
3
    background: salmon;
4
    color: white;
5
}
6

7
figure:has(.theme:checked):has(img) {
8
    background: limegreen;
9
}
10

11
figure:has(.theme:checked):has(img):has(h3)  {
12
    background: plum;
13
}
14
/** Not only target the figure but also get the element inside and apply the needed style */
15
figure:has(.theme:checked):has(img):has(h3) img {
16
    box-shadow: 3px 3px 4px rgba(0,0,0,0.8);
17
}
18

Each of those boxes will receive a different background colour based on the content in the boxes.

Some title

One lonely paragraph

Not a title

Paragraph with image

Random kitten generator

Foo title

Paragraph with image

Random kitten generator

:not()

The :not() is a negation pseudo-class and is probably most confusing from this post. There are a few quirks to keep in mind.

Specificity is similar to :is() but can increase as well. As per MDN:
"For example, #foo:not(#bar) will match the same element as the simpler #foo, but has the higher specificity of two id selectors."

Usage is similar to :has(). Add all the selectors you want to negate against as a comma-separated list.

If you use it globally like this :not(.foo), it will match anything that is not .foo including HTML and body tags

Example of :not() usage.

1

2
ul :not(li:first-child, li:last-child) {
3
    color: red;
4
}
5

6
a:not([href = "https://shvarcs.com"]) {
7
    color: green;
8
}
9

Select all but the first and last element.

  • First element
  • Next element
  • Next element
  • Next element
  • Next element

Add green styling for any link tag except if it is shvarcs.com, which will receive global styling from the website.

www.google.com
www.shvarcs.com
MDN

References