The gotchas of CSS Nesting
Tags: css, webdev, tutorial
Published: 2023-06-13 10:53:40

I've [written before](https://kilianvalkhof.com/2021/css-html/css-nesting-specificity-and-you/) about the problems you can run into with CSS nesting (keep in mind that article uses an older syntax but the point still stands) and the question that [@ChallengeCSS](https://twitter.com/ChallengesCss/status/1668536462906621953) tweeted out today made me realize there's actually a few more gotcha's. Here's what [they tweeted](https://twitter.com/ChallengesCss/status/1668536462906621953):

> Everyone is exited about CSS Nesting but are you ready for it? Answer the below quiz 👇

> What would be the result of the following code (without cheating! 😈)

> ```
> body {
>  @​media all {
>    background: red;
>  }
>  background: blue;
> }
> ```

Take a moment to come up with your own answer (or vote), then read on.

Now, I initially got it wrong. Here was my thinking pattern:

1.  @media doesn't add specificity, so both declarations have a specificity of 0,0,1
2. `background: blue ` comes later, so it wins

But no, the background is red! It turns out that has to do with the way browsers transform your nested CSS rules to individual rules it can apply. So lets dive into how browsers do that.

### A related gotcha: `:is()`

Last week was CSS Day (which was _amazing_) and of course a bunch of the presentations mentioned CSS Nesting. Unfortunately, some had a simplified explanation of how rules get resolved. 

The `&` in nested CSS isn't just replaced by the ancestor, which is what you might think, but it's ancestor is also wrapped in `:is()`:

```css
body {
    & div {
        ...
    }
}

/* Doesn't become this: */
body div { ... }

/* It becomes this: */
:is(body) div { ... }
```

Now that doesn't sound like there is much of a difference between `body div` and `:is(body) div`, indeed both have a specificity of 0,0,2, but remember that`:is()` takes on the highest specificity of the selectors in it. So when you have the following:

```css
main, #intro {
    & div {
        ...
    }
}
```

The resulting selector, even when targeting a `div` in `main`, ends up as:

```css
:is(main, #intro) div { ... }
```

Which makes it go from 0,0,1 for `main div` to 1,0,1 making it vastly more specific. That gotcha gets lost when examples fail to include the way ancestors are wrapped in `:is()` (and yes, they also nest :`is()`!)

## Back to the original gotcha

So back to the challenge up top. You can intermingle properties and nesting. You shouldn't to keep your code readable, but the following CSS works just fine and applies all the styling:

```css
body {
  filter: blur(5px);
  
  @media all {
    background: red;
  }
    
  background: blue;
  
  @media all {
    color: deeppink;
  }
  
  rotate: 20deg;
}
```

It's when the browser parses this CSS into individual rules that the sneaky thing happens:

1. The browser adds a new ruleset, `body`, and starts adding the properties to it/
2. The browser then adds another new ruleset for the nested media query and starts adding its properties to it/
3. When it exits the nested media query, it adds the rest of the properties to the original ruleset again until that is exited.

So if we look at this CSS again:

```css
body {
    @media all {
        background: red;
    }
    background: blue;
}
```

That actually resolves to these two rules in this specific order:

```css
body {
    background: blue;
}
@media all {
    :is(body) {
        background: red;
    }
}
```

Now from this CSS, it makes more sense that red wins. While it has the same specificity, it comes after the first rule so it wins. And that's the gotcha.

_This post originally claimed that only `@-rules` could be intermingled with properties but this was incorrect. Thanks [@ChallengesCSS](https://twitter.com/ChallengesCss/status/1668577684585164801) for correcting me._