css-not-is-where-has

Logical Selectors in CSS :not, :is, :where and :has

  • 4 min

Logical and relational selectors like :not(), :is(), :where(), and :has() are pseudo-classes for expressing selection conditions in modern CSS.

If you’ve made it this far, you already know how to select by tag, class, ID, state, and position. With that, you can do a lot. Still, CSS tends to become full of repeated selectors, scattered exceptions, and classes that only exist to save a specific rule.

These pseudo-classes help us write selectors that better express what we want: this one yes, this one no, any of these, or this element if it contains that.

They are one of the best and most useful additions ever made to CSS. So let’s dive into them in detail 👇.

Excluding with :not()

The pseudo-class () selects elements that do not meet a condition.

.menu a:not(.activo) {
    color: #64748b;
}
Copied!

In this example, we style all links in the menu except the one with the .activo class.

We can also use it to avoid applying a margin to the last element of a list.

.lista li:not(:last-child) {
    margin-bottom: 0.75rem;
}
Copied!

That reads quite nicely: “all li elements that are not the last child.” No need to add a .ultimo class in the HTML, which would be a bit sad.

:not() is very handy for small exceptions. If the selector starts getting too long, you’re probably better off using a clearer class.

Grouping options with :is()

The pseudo-class () allows grouping several options without repeating half a line of selector.

article :is(h2, h3, h4) {
    margin-top: 2rem;
}
Copied!

Without :is(), we would have to write:

article h2,
article h3,
article h4 {
    margin-top: 2rem;
}
Copied!

The difference may not seem huge here, but with long selectors it reduces quite a bit of noise.

:is(header, main, footer) :is(h1, h2) {
    color: darkblue;
}
Copied!

This rule targets h1 and h2 elements that are inside header, main, or footer. It’s the same idea as writing all the combinations by hand, just without turning the stylesheet into a comma soup.

Lowering specificity with :where()

() looks very similar to :is(), but it has a very useful feature: it does not add specificity.

:where(article, section) :where(h2, h3) {
    margin-top: 2rem;
    color: #1f2937;
}
Copied!

This fits perfectly in global stylesheets, where we want to provide a reasonable baseline without creating heavy selectors. Later, if a specific component needs to change that color, a simple class can override it without fighting against specificity.

.card h2 {
    color: #dc2626;
}
Copied!

The practical difference is this: :is() groups and maintains the specificity of the strongest selector inside it. :where() groups, but its specificity is always zero.

Selecting by content with :has()

The pseudo-class () allows selecting an element based on what it contains or what is near it (it’s the best invention in the history of CSS 😍)

For a long time, CSS could only go from parents to children. We could select .card img, but we couldn’t select .card depending on whether it had an image inside.

With :has(), this can now be expressed directly.

/* Selects .card only if it has an image inside */
.card:has(img) {
    background-color: #f0f0f0;
    border: 2px solid blue;
}
Copied!

This opens up very interesting patterns for components, forms, and visual states.

/* The form changes appearance if it has an invalid input */
form:has(input:invalid) {
    border-color: red;
    box-shadow: 0 0 10px rgba(255, 0, 0, 0.2);
}
Copied!

We can also use it to enhance small components without adding extra classes to the HTML.

.campo:has(input:focus) {
    border-color: #2563eb;
    background-color: #eff6ff;
}
Copied!

Here the .campo container reacts when the inner input receives focus. Before, this used to require JavaScript or manually added classes.

Combining them wisely

These pseudo-classes can be combined with each other. This allows writing quite expressive conditions, as long as we don’t overdo it.

section:has(.comprar) :is(h2, h3):not(.oculto) {
    color: #92400e;
}
Copied!

The rule reads like this: inside a section that has a .comprar element, select h2 or h3 titles that don’t have the .oculto class.

We can also use :where() to leave base styles that are very easy to override.

:where(.post, .pagina) :is(h2, h3):not(:first-child) {
    margin-top: 2rem;
}
Copied!

Don’t try to show off everything you know in a single selector. If a rule needs five minutes of meditation to understand, it’s probably better to use a class.