Kilian Valkhof

Building tools that make developers awesome.

CSS Nesting, specificity and you

CSS & HTML, 4 August 2021, 9 minute read

Native CSS nesting is coming to browsers soon. With nesting, that you might be familiar with from Sass or Less, you can greatly cut down on writing repetitive selectors. But you can also really work yourself into a corner if you’re not careful. This is an overview of how you can already use it today, the pitfalls and how to avoid them.

What does CSS Nesting look like?

If you come from Sass or Less, you might be familiar with nesting that looks like this:

div {
  background: #fff;
  p {
    color: red;
  }
}

That same block written in natively nested CSS looks like this:

div {
  background: #fff;
  & p {
    color: red;
  }
}

You’ll notice there is now an “&” in front of the p. That is the “nesting selector”, and it tells the browser you want to nest the p in the div. You can only add the nesting selector at the beginning. We also have that in Sass, but you only need it when you want to add something to the earlier selector, like a class:

div {
  background: #fff;
  &.red {
    color: red;
  }
}

And this is actually exactly the same in native CSS nesting. Think of it as if the & is just enforced for each selector. Just remember that in CSS, the & always comes first.

Now, there is a way to place the & somewhere else, and that’s by using the nesting at-rule, @nest. By prefixing a selector with @nest you can choose where you want the selector to be nested.

div {
  background: #fff;
  @nest section & {
    color: red;
  }
}

In the code above, you’re not giving the section the color red, but you’re creating a new selector section div (the & is replaced by the selector above it) that has a css styling.

You can only add nesting selectors after all your regular CSS. Any CSS properties after the nesting selector will be ignored. So the following is invalid:

div {
  background: #fff;
  & p {
    color: red;
  }
  border: 1px solid;
}

Lastly, you can nest media queries as well, by making sure you use the nesting selector to restart a “rule set” block.

div {
  flex-direction: column;
  
  @media (min-width: 40rem) {
    & {
      flex-direction: row;
    }
  }
}

Like before the & will be replaced with its parents selector, so the code above ends up being the same CSS as this:

div {
  flex-direction: column;
}
@media (min-width: 40rem) {
  div {
    flex-direction: row;
  }
}

Using it today with postcss-preset-env

CSS Nesting is not yet supported in all browsers, but if you use PostCSS you can install the PostCSS Preset Env plugin and PostCSS will convert your nested CSS to CSS that’s understood by browsers today.

To use PostCSS you need a build step, for example with Webpack, Parcel or Gulp. You can find an overview of setups in the PostCSS docs on usage, so pick something that works for you.

Try it out

To play around with PostCSS Preset Env, check out their playground, which compiles things on the fly for you.

A small example

An easy way to get started with it in your own project is to use the command line interface by installing the PostCSS CLI and postcss-preset-env:

npm install postcss postcss-cli postcss-preset-env --save-dev

And creating a postcss.config.js file that looks like this:

const postcssPresetEnv = require('postcss-preset-env');

module.exports = {
  plugins: [
    postcssPresetEnv()
  ]
}

Then running postcss in your terminal in the same folder as your config file, and it will automatically pick it up.

 postcss path/to/input.css -o path/to/output.css 

Okay, so thats how you can use it today, but when you do there’s something to be aware of, and that’s specificity.

Specificity: the big pitfall

To understand what is happening here, I have to admit that in the nesting examples above I made things a little simpler than how they really are.

When a selector is resolved, the browser wraps an is() selector around any parent selector, and that influences specificity.

The is() selector

If you’re unfamiliar with it (it’s relatively new), the is() selector lets you wrap a bunch of different selectors to prevent duplication:

main h1,
main h2,
main h3 { 
  font-family: fantasy;
}

main :is(h1, h2, h3) {
  font-family: fantasy;
}

The two examples give the same result but notice how in the second example we’ve only written “main” once while in the first example we had to write it three times. When it comes to specificity, the :is() selector gets its specificity from the most specific item in the list.

Unfamiliar with specificity?

If an element is targeted by multiple selectors, browser use the specificity of the selector to determine which styling to apply. Each selector has a specificity that is determined by what you use in that selector: elements, ID’s, classes etc.

It usually looks like three buckets, like so: 1.2.3. The first is the number of id’s, the second the number of classes, pseudoclasses and attribute selectors, and the last bucket has elements and pseudo-elements.

A single value in a higher level is more important than all levels below it. For example, you can have one selector with a 1000 class names, and one selector with a single ID, the latter would still be more specific.

If you want to get a better feel for specificity, I built a CSS specificity calculator that supports selectors all the way up to level 5. Type in your selectors and see what specificity they end up with.

Back to the :is() selector

So for our :is(h1, h2, h3) example, the specificity is low, 0.0.1, since there are only elements in there. But for the selector :is(#start, h1), the specificity of the entire :is() declaration shoots up to 1.0.0 because of the id.

This is easy to spot if you have to type out the entire selector each time, but the cool thing about nesting is that you no longer have to do that.

That unfortunately means you can end up with massively specific CSS selectors and not know it!

For example, lets look at this “simple” example of a list of newsitems. Because they’re their own component it makes sense to keep everything together.

main {
  /* css properties */
  & #intro {
    /* css properties */
    & .newsitems {
      /* css properties */
      & div.newsitem {
        /* css properties */  
        & h2 {
          /* css properties */
        }
        & p {
          /* css properties */
          &.meta {
            /* css properties */
            & span.date {
              /* css properties */
            }
          }
          & + p {
            /* css properties */
          }
        }
        & a {
          /* css properties */
        }
      }
    }
  }
}

Because each selector on its own looks so simple, and you can just keep nesting, before you know it you’re looking at an actual css rule that looks like this:

main #intro .newsitems div.newsitem p.meta + p { }

Which has a specificity of 1.3.4 and will overwrite most other CSS selectors you write.

Do this often enough and before you know it you’ll find yourself creating even more complex selector or adding in !important somewhere to get it to work correctly and by then, you’ve lost.

Prevent specificity issues

As tempting as it is to just keep nesting (it’s so easy! And you have to type so little!), to prevent specificity issues you want to limit how deep you nest and break out of nesting when you can.

Limit the nesting

Tooling can help you by warning you when you nest too deep. For stylelint there is the max-nesting-depth rule, for example.

Completely unscientifically, but looking at my own code setting the limit to 3 seems to work the best. This lets you define and style a top level container, its children and its children’s children.

Break out of nesting

If you limit how deeply you can nest something, you also want to break out of nesting as soon as possible. Think of nested CSS as a self-contained set of styling, almost like a component. If one of the child elements should be their own component, also start a new nesting “context”.

If we take the example above, I would say the following items all deserve their own new context:

But since each of these are in their parent, where do you add their own styling? In their own block, or nested in their parent.

To make that a bit clearer, lets say the newsitems block has three newsitems in it and we want them in a horizontal flex layout, so the items are side by side.

<div class="newsitems">
  <div class="newsitem">
    <h2>Title</h2>
    <p class="meta">posted at <span class="date">date</span>
    <p>Description</p>
    <a href="">Link</a>
  </div>
  <div class="newsitem">
    <h2>Title</h2>
    <p class="meta">posted at <span class="date">date</span>
    <p>Description</p>
    <a href="">Link</a>
  </div>
  <div class="newsitem">
    <h2>Title</h2>
    <p class="meta">posted at <span class="date">date</span>
    <p>Description</p>
    <a href="">Link</a>
  </div>
</div>

For the .newsitems, we set up a flex layout:

.newsitems {
  display:flex;
  justify-content: flex-start;
  align-items: flex-start;
  gap: 1rem;
}

To make the newsitems all the same width (mostly) we can add flex: 1 1 33% to them, but that only makes sense in the context of .newsitems. We might use a single .newsitem elsewhere, and so the “flex” isn’t part of the styling for the individual news item. While we do that, we can make the selector we use slightly less specific by changing & .newsitem to & > div.

.newsitems {
  display:flex;
  justify-content: flex-start;
  align-items: flex-start;
  gap: 1rem;
  
  & > div {
    flex: 1 1 33%;
  }
}

.newsitem {
  /* css properties */
  & h2 {
    /* css properties */
  }
}

Unfortunately, now we have two places where styling happens and they might not be so neatly side-by-side in your own code. Ideally, we would only style each element once to keep a better overview.

A potential issue is that .newsitems > div is more specific than .newsitem (0.1.1 versus 0.1.0) so any styling in the single .newsitem would be overwritten by CSS in the nested selector. Usually this is what you want, but not always.

A potential way to work around this is to be more strict in where your styling is, and there’s a couple of ways, each with benefits and with downsides.

Split between layout styles and component styles

Some CSS is used to define the layout, like position and flex, while others are used to define the style, like background and font-size. You can keep the layout logic in the nested selector and the styling logic in the top level selector:

.newsitems {
  display:flex;
  justify-content: flex-start;
  align-items: flex-start;
  gap: 1rem;
  
  & > div {
    flex: 1 1 33%;
  }
}

.newsitem {
  background: #fff;
  color: #332;
  /* more CSS properties */
}

Now the style of the component is decoupled from its layout in a parent component, making it more flexible. This does require some discipline and you’ll have to look in different places depending on whether you want to change the layout or the style.

Keep empty parents

When an element is only ever used in one parent element, you could keep the top level of your nesting empty and only use it to define the selector, like so:

.newsitems {
  /* no styling here */
  & > div {
    flex: 1 1 33%;
    background: #fff;
  }
}

.newsitem {
  /* no styling here */
  & h2 {
    /* css properties */
  }
}

This way, you’re always styling the contents of a component (as it were) and using the nesting capabilities purely as a means of organising your CSS.

The downside here is that it won’t always work, and if the same .newsitems are also shown in a sidebar somewhere you end up duplicating all their styling for the sidebar. While some of that may differ, you might end up with duplication that could be avoided.

Style components stand-alone

This is essentially the inverse, where all your styling happens inside a component, and you use additional classes to deal with placement in different parents:

.newsitem {
  background: #fff;
  
  &.in-newsitems {
    flex: 1 1 33%;
  }
  &.in-sidebar {
    flex: 1 0 100%;
  }
  
  & h2 {
    /* css properties */
  }
}

This keeps everything co-located but does add the complexity of having to use additional classes depending on where your component is placed, so you can’t simply drop HTML in another place of the DOM.

Which of these three works best depends on what you prefer and the project you’re working in. The important thing to take away is that it pays off to be intentional about how you use CSS Nesting. Make it a free for all and you’ll end up with CSS that’s very difficult to maintain, but used intentionally it’s a wonderful way to organize your CSS and repeating yourself less.

Fingers crossed it lands in browsers soon!

Polypane browser for responsive web development and design Hi, I'm Kilian. I make Polypane, the browser for responsive web development and design. If you're reading this site, that's probably interesting to you. Try it out!