Simplify CSS selectors with :is() and :where()
Tags: css, webdev, tutorial, html
Published: 2023-01-19 18:00:00

* [Introduction](#intro)
* [How Both :is() and :where() Work](#work)
    - [How These Help Minimize Errors](#errors)
* [Specificity: How :is() and :where() Differ](#differ)
    - [:where() has Zero Specificity](#zero)
    - [:is() Applies the Specificity of the Highest Selector](#highest)

## Introduction <a name="intro"></a>
The new pseudo-classes `:is()` and `:where()` help make compound selectors more efficient and less error-prone by allowing you to group and condense selectors, which makes them easier to maintain, but each has its own quirks with how it calculates specificity.

Keep in mind that while compound selectors in CSS are essential when learning about CSS and how the cascade works, in practice they are somewhat controversial. Sometime during the almost 10 years that I've been a web developer, they became frowned upon because they were difficult to maintain on large projects with multiple developers. Methodologies like BEM became popular in part because they avoided compound selectors entirely (and therefore removed the need to figure out CSS specificity on the fly). These pseudo-classes are still important to learn, however, since compound selectors are vital to CSS, and may also potentially be incorporated in other future CSS functionality, like nesting.

Let's walk through examples of how both of these pseudo-classes work. If you would like to test the CSS as you read, here is the HTML:

```html
<header>
  <h1>About Us</h1>
  <h2>Meet our Team</h2>
  <ul>
    <li>Unordered List item one</li>
  </ul>
  <ol>
    <li>Ordered List item one</li>
  </ol>
</header>
<main>
  <article>
    <h3>Web Designer</h3>
    <p>This is our web designer.</p>
    <a href="#">View Bio</a>
  </article>
  <article class="card">
    <h3 id="heading">Web Developer</h3>
    <p>This is our web developer.</p>
    <a href="#">View Bio</a>
  </article>
</main>
<footer>
  <ul>
    <li>Unordered List item one</li>
  </ul>
  <ol>
    <li>Ordered List item one</li>
  </ol>
</footer>
```

## How Both :is() and :where() Work <a name="work"></a>

With both `:is()` and `:where()`, you can group any selectors within the parentheses where you want to have the same declarations applied. 

**For instance**:

```css
h3,
p,
a {
   color: blue;
}
```

**Becomes**:

```css
:where(h3, p, a) {
   color: blue;
}
```

The pseudo-classes become more powerful once you combine more complex selectors. For example, you can condense the following compound selectors:

**Before**:
```css
.card h3,
.card p,
.card a {
   color: blue;
}
```

**After**:
```css
.card :is(h3, p, a) {
   color: blue;
}
```

You can even chain them:

**Before**:
```css
header ul,
header ol,
footer ul,
footer ol {
  color: green;
}
```

**After**:
```css
:where(header, footer) :where(ul, ol) {
  color: green;
}
```

### How These Help Minimize Errors <a name="errors"></a>

CSS is a declarative language; if the browser runs across a line that it doesn't understand, it skips it and keeps going. If we have the following CSS rule:

```css
.card h3,
.card p {
   color: blurple;
   font-family: sans-serif;
}   
```

The color `blurple` doesn't exist, and that declaration would be skipped, but then the `font-family: sans-serif` would still apply.

However if you had the following CSS:

```css
.card h3,
.card p,
.card :second-child {
   color: blue;
   font-family: sans-serif;
}
```

There is no such thing as a `second-child` selector, and this **ENTIRE** rule would fail silently, meaning neither `color` nor `font-family` will apply to any selectors even though both declarations are valid. The `.card :second-child` selector ruins the entire rule.

#### Forgiving Selectors
Here's where the other benefit to using these new pseudo-classes comes in. Each pseudo-class accepts a "forgiving selector list", which means each 

> "parses each selector in the list individually, simply ignoring ones that fail to parse, so the remaining selectors can still be used." - [CSS Working Group](https://drafts.csswg.org/selectors-4/#typedef-forgiving-selector-list)

If you had instead written the previous code with either `:is()` or `:where()`, you would have a better outcome:

```css
.card :where(h3, p, :second-child) {
   color: blue;
   font-family: sans-serif;
}
```

Only the last selector `:second-child` fails due to the "forgiving selector list", so both `.card h3` and `.card p` still get the CSS declarations of `color` and `font-family` applied.

## Specificity: How :is() and :where() Differ <a name="differ"></a>

The major difference between `:is()` and `:where()` is how they handle specificity in regards to the selectors within the parentheses. Specificity determines which rules will get applied to selectors.

### :where() has 0 Specificity <a name="zero"></a>

Any selectors using `:where()` will have 0 specificity. Put very simplistically, HTML tags have a specificity of 1, classes are 10, and IDs are 100. The calculations actually use 3 columns but this is just so you understand that almost all selectors have a specificity greater than 0. Generally you won't need to calculate exact specificity when writing CSS, but you can see that this means that anything inside `:where()`, will not apply if that specific selector has already been styled somewhere else beforehand.

```css
/* specificity 0-0-1 */
article {
  color: blue;
}

/* This rule will not apply 
because it has lower specificity (0) 
than the rule above */
:where(article) {
  color: red;
}
```

This makes `:where()` a good choice for CSS resets or any base styling that may need to be easily overridden later.

### :is() Applies the Specificity of the Highest Selector <a name="highest"></a>

The pseudo-class `:is()` will take the highest specificity of whatever is in your selector list, and then apply that to all the selectors. This means for example that if you group a tag selector with an ID selector, the tag now has the same calculated specificity as the ID. You will not be able to override the styling for the tag afterwards unless you increase the specificity to be at least the same for a CSS ID.

```css
article :is(p, a, #heading) {
  color: blue;
}

/* This rule will not apply
because the ID selector in the previous rule
gives the anchor tag a higher specificity than here */
article a {
  color: red;
}

/* Let's force the override with the dreaded IMPORTANT  */
article a {
  color: red !important;
}
```
 
The specificity of `is()` makes it hard for me to think of a proper use case for it. If you are not careful with your selectors, such as mixing tags, classes, and IDs together, you will spend more time debugging why certain styles are not applying. If you make sure to **only** group similar selectors together (all having the same specificity), then this may be useful to use in lieu of the traditional compound selector syntax.