Dec 20

CSS Nesting

With CSS Nesting native in browsers, the amount of CSS you need to write can be lowered drastically while making it easier to maintain and reason about.

By Kilian Valkhof

CSS Nesting is a new syntax for CSS that lets you nest selectors inside of other selectors, where every nested selector is relative to their parent.

In its simplest form, it looks like this:

.card {
border: 2px solid red;
padding: 1rem;

.card-title {
font-family: Jolly;
font-size: 3rem;
}
}

A browser will take the style for an element with the class card-title and apply it only if that element is nested inside an element with the class card. In other words, a browser will interpret it like this:

.card {
border: 2px solid red;
padding: 1rem;
}

.card .card-title {
font-family: Jolly;
font-size: 3rem;
}

Benefits of Native CSS Nesting

Now some relevant implementation details complicate things that we'll get to later (this is the web, after all!), but this example is enough to go over the benefits that CSS nesting brings:

Less typing

In the "interpreted" version above, you can see that the browser duplicated .card for us, but in our source, we only had to write it once. By nesting selectors, you no longer have to repeat a part of your selector if you want to prevent your styles from applying too broadly. In addition, less typing also means your files end up being smaller, and, therefore faster to download.

Easier scoping

That also means that it's easier to scope. If you're working on a card, you only have to write that .card selector once and nest all the relevant CSS inside it. That means you'll need to write less specific selectors since they're already scoped to their parent. This also makes your code much more portable, as you don't have to go through your CSS to find all the styles that apply to the card but can take .card and everything in it instead.

Your CSS follows the DOM

A benefit of this is that your CSS starts to mimic your DOM. In the above example, card-title is nested both in the CSS as well as in the DOM. When your CSS mimics your DOM, it's much easier to reason about, and easier to understand what effects your changes will have.  If you've ever worked on a large CSS codebase, you know how dangerous deleting CSS can be even if you think you no longer use it.

When your CSS follows your HTML structure, you're able to delete things with much more confidence. For example, when you remove the card-footer from your card HTML, you know that your .card-footer CSS can only apply to that part of your site, so you can remove it from the CSS as well.

Nesting Complexities

Now that you know what makes CSS Nesting so useful, let's explore some of the complexities.

Versions

At this point, we need to point out that there are currently two versions of CSS nesting: the strict version, which is what Chromium up to 119 and Safari up to 17.1 implement, and the "relaxed" version, which is what Firefox shipped with and is now available in Safari 17.2 and Chromium 120.

The difference between them lies in how they deal with nesting element selectors (like p, div). The strict version allows for any selector that begins with a non-letter to be nested without the "nesting selector", the &. That means that instead of this:

.card {
border: 2px solid red;
padding: 1rem;

h3 {
font-family: Jolly;
font-size: 3rem;
}
}

You have to write this:

.card {
border: 2px solid red;
padding: 1rem;

& h3 {
font-family: Jolly;
font-size: 3rem;
}
}

Or your h3 style won't be applied. The reason for this is that properties (like margin) also start with a letter so the browser's CSS engine wouldn't know if you were adding a new property or a new nesting selector. 

Browsers found a way around that, so the relaxed version allows you to nest element selectors without the &, making everything a lot simpler. By the time you read this article, Chromium 120 and Safari 17.2 will have been released, so all browsers now support the relaxed version.

I would suggest you keep using the strict version, though, since that will keep working across all browsers.

The nesting selector and how nesting gets resolved by the browser

So, does that mean you don't need the nesting selector anymore? For simple nesting no. But the nesting selector lets you control how nesting should happen. To understand how it does that, you need to know three things:

Let's go over these in order.

Implicit Nesting selector

Going back to our first example, the nested .card-title doesn't have a nesting selector, so the browser will add one implicitly:

.card {
border: 2px solid red;
padding: 1rem;

(implicit '&') .card-title {
font-family: Jolly;
font-size: 3rem;
}
}

On its own, this doesn't matter a lot, but it's important to keep in mind when you know that the & gets replaced.

Resolved selector

The & gets replaced with "is(<parent selector>)" so the nested selector in the code above ends up not being .card .card-title as we said at the beginning of this article, but as :is(.card) .card-title.

In this example, that doesn't matter a lot because both of them have the same specificity (0,2,0), but if you have comma separated selector as a parent, you could accidentally be making a rule with a very high specificity:

.card,
#xmas-card
{
.card-title { ... }
}

This gets resolved to this: :is(.card, #xmas-card) .card-title which has a specificity of (1,1,0), because the :is() pseudo selector will get the specificity of the most specific item, in this case, the id. And that's also the case if your .card-title isn't in the #xmas-card at all but in a regular .card.

This can get even trickier if you realize that the :is() selectors also get nested. Let's go over how this works with the following three-levels-deep nesting:

.card {
.card-body {
p { ... }
}
}
  1. First, the "p" is implicit & p.
  2. That gets replaced with the parent, which for convenience, we'll also add the & to, creating :is(& .card-body) p.
  3. Then lastly, we also fill in the "&" there, replacing it with :is(.card) resulting in :is(:is(.card) .card-body) p which is what your browser ends up using.

Adding an & selector

You can be explicit in how you nest by adding the & selector. I personally think this is more readable, but beyond that, it can also affect how your nesting works. That's because the space is also a type of selector: the descendant selector. And so, whether or not you add a space after your & can make a difference.

The following code will apply when you hover the card:

.card {
&:hover { ... }
}

That is because it resolves to :is(.card):hover.

The following code will do something different:

.card {
& :hover { ... }
}

Because of the space, the :hover part is now targeting descendants of cards and so acts like :is(.card) *:hover. Pseudo-classes like :hover get an implicit * (universal element selector) before them. So keep that space in mind.

By adding an explicit & selector somewhere else in the selector, you can target different parts of your nesting. For example, to style cards but only if they're in a list, you could do this:

.list {
.card { ... }
}

The downside is that all the styling of your card components would also have .list as a parent selector, making it more specific and less modular.

Instead, you could write this:

.card {
.list & { ... }
}

This way, all your other card styling could be scoped to the card, and the extra styling when your card is in a list would be included with it, keeping everything nice and modular.

Adding multiple selectors is useful if you want to style elements in a sequence. For example, to add margin to each card except the first when they're in a list:

.card {
.list & + & {
margin-top: 1rem;
}
}

This resolves to .list :is(.card) + :is(.card), or written more simply, .list .card + .card

Nesting At-rules

A very useful part of CSS Nesting is that you can also nest at-rules like @media, @supports, and friends.

Instead of writing this:

.card {
max-width:100%;
}

@media (min-width: 50rem) {
.card {
max-width: 40rem
}
}

You can nest that media query:

.card {
max-width:100%;

@media (min-width: 50rem) {
max-width: 40rem
}
}

Notice how we don’t have to repeat .card and that we can use the CSS declarations directly in the media query because the browser implicitly reads it as this:

.card {
@media (min-width: 50rem) {
& {
max-width: 40rem
}
}
}

And as we learned previously, that & is then replaced with :is(.card).

Things to watch out for when nesting

Back when I first used Sass, I went wild with nesting, sometimes nesting 20 levels deep. It’s very tempting just to keep going as you build out your styling. But each nesting level also increases the specificity of your selector, making it harder to edit later (not to mention the indenting!).

A simple rule of thumb is to see if you can break out into a new set of nesting when you reach 3 or 4 levels deep. This makes your selectors easy to reason about, and there is probably a logical break for your component and its sub-components. There are multiple other strategies to deal with this, such as keeping empty parent selectors and splitting between layout and style. I explore these strategies in CSS Nesting, specificity and you.

Another tricky part is that because nesting gets resolved to un-nested code, the order you write CSS in might not be the order your CSS gets applied. I wrote about this in “The gotchas of CSS Nesting”. The short of it is: You can nest at-rules before applying regular properties, like so:

body { 
@media all {
background: red;
}

background: blue;
}

If you follow your CSS order, the background would be blue, because at-rules add no specificity and background: blue comes last.

Once the browser finishes computing CSS values, it ends up applying the rules like this and moving the media query last:

body {
background: blue;
}

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

Now background: red is last, which ends up being the applied background color.

The best way to deal with this, and to keep your code easy to reason about is to only nest after all the regular properties and not to mingle them even though it's allowed by the nesting syntax.

That’s CSS Nesting

Nesting is available in all browsers, though you’ll want to keep using the strict notation to keep the widest compatibility.

Nesting helps you keep your CSS smaller, easier to reason about, and more closely follow the DOM that you’re styling. When using Nesting, try not to nest too deeply, and keep in mind that your CSS needs to be resolved, which can change both the order and the selectors your browser ends up using.

If you want to learn more about nesting, here are some additional resources:

Kilian Valkhof

Kilian Valkhof

Kilian is a web developer from the netherlands, building Polypane.app, the browser for developers.

Kilian selected Giving Green Fund for an honorary donation of $50

Giving Green Fund

The climate crisis sits at the heart of many problems around the world, increasing inequality and disproportionately affecting those least responsible for it. If we want to keep earth livable for our children, we need to fight it at many fronts. Giving Green is a fund that research projects and funds those most effective.