Modern CSS selectors
Tags: webdev, css
Published: 2022-08-11 23:46:53

by [Craig Buckler](https://blog.openreplay.com/authors/craig-buckler)

CSS selectors target specific HTML elements for styling. This basic example locates all `<p>` paragraph elements and changes the text color to red:

```
p {
  color: red;
}
```

CSS selectors have become increasingly sophisticated since the introduction of CSS3 more than a decade ago. This tutorial discusses three recent pseudo-class selectors which target elements based on their state.


## `:is()` Pseudo-Class Selector

*(Note that older articles may refer to `:matches()` or `:any()` but the CSS standard settled on `:is()`.)*

Selectors which target different elements with the same styles can lead to verbose CSS. In this example, `<p>` paragraph text defaults to black but any `<p>` within a `<main>`, `<header>`, or `<footer>` is green:

```
/* default */
p {
  color: black;
}

/* <p> in <main>, <header>, or <footer> */
main p,
header p,
footer p {
  color: green;
}
```

SASS and similar CSS pre-processors permit nesting:

```
main, header, footer {
  p {
    color: green;
  }
}
```

This creates identical CSS code and reduces typing effort but:

- You require a CSS build tool. Native CSS nesting is coming but will [not receive good browser support until 2023 or later](https://caniuse.com/css-nesting).
- Nesting introduces its own set of problems. Deeply-nested selectors are difficult to read and can output unnecessarily verbose CSS.


The `:is` pseudo-class provides a native CSS solution:

```
:is(main, header, footer) p {
  color: green;
}
```

`:is()` has [full support in all modern browsers](https://caniuse.com/css-matches-pseudo) -- just Internet Explorer is missing.

Any number of `:is()` selectors can go anywhere, and each may contain any number of other sectors. The following code colors `<h1>`, `<h2>`, and `<p>` elements red that are children of a `<section>` with a class of `.primary` or `.secondary` that is not the first child of an `<article>`:


```
article section:not(:first-child):is(.primary, .secondary) :is(h1, h2, p) {
  color: red;
}
```

You would require the following six CSS selectors without using `:is()`:

```
article section.primary:not(:first-child) h1,
article section.primary:not(:first-child) h2,
article section.primary:not(:first-child) p,
article section.secondary:not(:first-child) h1,
article section.secondary:not(:first-child) h2,
article section.secondary:not(:first-child) p {
  color: red;
}
```

Note that `:is()` can NOT match pseudo-elements such as `::before` and `::after`:

```
/* this fails */
p:is(::before, ::after) {
  display: block;
  content: 'pseudo';
}
```


## Open Source Session Replay

_[OpenReplay](https://github.com/openreplay/openreplay) is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data._

![replayer.png](https://static.openreplay.com/images/banner-blog.png)

_Start enjoying your debugging experience - [start using OpenReplay for free](https://github.com/openreplay/openreplay)._




## `:where()` Pseudo-Class Selector

The `:where()` pseudo-class selector syntax is identical to `:is()`. It has the [same level of browser support](https://caniuse.com/mdn-css_selectors_where) and will often produce the same result:

```
:where(main, header, footer) p {
  color: green;
}
```

The difference is *specificity*:

* `:is()` uses the specificity of the most specific selector in its arguments, but
* `:where()` has a specificity of zero.

Consider this HTML:

```
<main>
  <p>main text</p>
</main>
```

The text will become green when applying the following CSS:

```
main p {
  color: black;
}

:is(main, header, footer) p {
  color: green;
}

:where(main, header, footer) p {
  color: blue;
}
```

The `:is()` selector has the same specificity as `main p`, but it comes later, so the text is green. Blue text is only applied when you remove both the `main p` and `:is()` selectors.

`:where()` has fewer use-cases than `:is()` but the zero specificity can be useful for CSS resets. Consider this reset which applies a top margin of `1em` to `<h2>` headings *unless* they're the first child of a `<main>`:


```
/* reset */
h2 {
  margin-block-start: 1em;
}

main :first-child {
  margin-block-start: 0;
}
```

Applying a custom `<h2>` top margin later in the CSS has no effect because `main :first-child` has a higher specificity:

```
/* this has no effect */
h2 {
  margin-block-start: 2em;
}
```

Using the zero specificity `:where()` means that any reset style can be overridden without resorting to additional selectors or `!important`:

```
/* reset */
:where(h2) {
  margin-block-start: 1em;
}

:where(main :first-child) {
  margin-block-start: 0;
}

/* this now works! */
h2 {
  margin-block-start: 3em;
}
```


## `:has()` Pseudo-Class Selector

The `:has()` selector uses a similar syntax to `:is()` and `:where()` but targets an element which contains a set of others. That's correct: web developers finally have a way to target parent elements -- such as all `<a>` link anchors which contain an `<img>` or `<div>`:

```
/* styles applied to the <a> element */
a:has(img, div) {
  border: 2px solid blue;
}
```

This opens possibilities that would have required JavaScript in the past. For example, you can set the style of an outer `<fieldset>` when any required inner field is not valid and disable any following submit buttons:

```
/* red border when any required inner field is invalid */
fieldset:has(:required:invalid) {
  border: 3px solid red;
}

/* disable following submit button */
fieldset:has(:required:invalid) + button[type='submit'] {
  opacity: 0.2;
  pointer-events: none;
}
```

`:has()` is newer than `:is()` and `:where()`. At the time of writing, [limited support is available in Safari 15.4+ and Chrome 105+](https://caniuse.com/css-has), due for release in late 2022.


## Conclusion

The `:is()` and `:where()` pseudo-class selectors simplify CSS syntax and lessen the need for a pre-processor such as Sass.

`:has()` is more exciting. Parent selectors have been among the top CSS developers' wishes for two decades. Still, browser vendors resisted for performance reasons (adding, removing, or modifying child elements can affect the styling of the whole page). We've become used to a world where parent selection was not possible, but it'll rapidly become a popular option when most browsers support `:has()` in 2023.


> A TIP FROM THE EDITOR: For a comparison of CSS frameworks, don't miss our [5 CSS-in-JS Frameworks to use in 2021](https://blog.openreplay.com/5-css-in-js-frameworks-to-use-in-2021) article.


[![newsletter](https://blog.openreplay.com/static/e6b18eda7a327511296a2f27692f9c70/e9957/newsletter.png)](https://newsletter.openreplay.com/)