Skip to main content

Native CSS Nesting vs. Sass Nesting

Summary: People coming from Sass (or other pre-processors) might feel like they know how CSS nesting works, but there are a few key differences!

Kevin Powell

Nesting has come to native CSS, and while I did a deep dive on it in a previous post, I want to highlight the difference between how it works in Native CSS and SCSS, because there are a few key differences.

  1. We cannot create concatenated selectors using native CSS, such as &__title
  2. Native CSS uses :is() under the hood, which can impact specificity, but also change how a selector works vs. Sass nesting
  3. We need an & before an element selector in native CSS nesting (for now)

I’ll take these in order, and also talk about how Sass is going to handle CSS nesting.

We cannot create concatenated selectors using native CSS

People love BEM, and Sass makes it really easy to do that by creating concatenated selectors like this:

.card {
  &__title { }
  &__body { }
  &__footer { }
  
  &--with-image { }
}

This doesn’t work with native CSS because the & is a live object, and both it and the extra text we add after it are considered separate selectors.

To help illustrate how this works would be if we did this:

div {
  .accent& { padding: 1rem; }
}

You’d probably never see this in Sass, as it would compile to .accentdiv.

In native CSS, this actually would turn into a potentially useful selector that would be equivalent to div.accent.

This is an equivalent, because the & is a live object that’s never compiled to anything, so it’s always treated as its own selector, as we can see in action in this codepen:

See the Pen CSS nesting & live object example by Kevin (@kevinpowell) on CodePen.

I wouldn’t suggest doing this for readability reasons, but it’s important to understand that the & is always a live object and it’s own selector.

The impact of :is()

Speaking of :is(), it can impact how our selectors work!

Specificity

:is() can have a big impact on the specificity of your selectors.

Let’s use this as an example:

#main, article {
  h2 {
    font-family: serif;
  }
}

If we are using Sass, the specificity is straightforward as this would compile to the following:

#main h2,
article h2 {
  font-family: serif;
}

With nave CSS nesting, however, the nested example above would be the equivalent of this:

:is(#main, article)  h2 {
  font-family: serif;
}

:is() has the same specificity as the highest selector inside of it, so in this case, the h2 has the same weight as an ID selector.

This is one of those things that probably won’t matter very much most of the time, but could trip you up somewhere along the way if you aren’t aware of it.

Differences in what gets selected

More than impacting specificity, another big difference is that the final output can be completely different!

For example, let’s say we started with this:

.call-to-action .heading {
  .dark-theme & {
    padding: .25rem;
    background: hsl(42, 72%, 61%)
    color: #111;
  }
}

With Sass, that would result in a rule which selects the .heading inside a .call-to-action, which is nested in an element with the .dark-theme class:

.dark-theme .call-to-action .heading {
    padding: .25rem;
    background: hsl(42, 72%, 61%)
    color: #111;
}

With native CSS, we would instead get the equivalent to this:

.dark-theme :is(.call-to-action .heading) {
    padding: .25rem;
    background: hsl(42, 72%, 61%)
    color: #111;
}

Granted, this is similar and would match the same thing as the Sass version, but it would also select the .heading in this case as well, which would not be work if we were using Sass:

<div class="call-to-action">
  <div class="dark-theme">
    <h3 class="heading"> </h3>
  </div>
</div>

This likely isn’t something to would trip you up too often, but it does feel like one of those edge cases that could get you stuck on something stupid for way longer than you’d like 😅.

See the Pen Different matches SCSS and CSS nesting by Kevin (@kevinpowell) on CodePen.

How is native CSS nesting going to work with Sass nesting?

Thankfully Sass is very aware that this could potentially throw some wrenches into the works, and they have a plan in place.

The main points are:

  • In the short term, nothing changes until :is() has 98% browser support,
  • When browser support for :is() hits 98%, they will update Sass to output :is() which means it will act in the same way as native CSS nesting
  • That update will be part of a major version release, as it will have breaking changes for older projects (they will be putting effort into getting their Sass Migrator to deal with those changes)
  • They have no plans to drop the &-suffix support, and will continue to support it even after the above changes (unless they are able to come up with a simple alternative)

Nesting inside .css files

When you @use files, you can include regular .css files, as well as .scss and .sass.

Moving forward, if you import a .css file it will leave any nesting in it as is, assuming that you want to keep it that way since it’s already in a regular .css file.

The & is required in native CSS nesting (for now)

This isn’t a big deal, but in Sass we can get away with this:

.nav {
  ul { ... }
  li { ... }
  a { ... }
}

If we want to do this with native CSS, we have to do it like this:

.nav {
  & ul { ... }
  & li { ... }
  & a { ... }
}

This is because nested selectors in native CSS must start with a symbol.

That means that we don’t need the & if the nested selectors are classes, IDs, pseudo-classes, etc, but we need the & before element selectors.

Or well, we do in Chrome and Safari, for now.

This was part of the original spec because of a limitation on the browsers parts, but a workaround was found, and the current implementation in Firefox doesn’t require the &, and it will be dropped from Chrome at one point too. I haven’t heard on progress from Safari, but one can only assume it’s also in the works.

I’ll update this post once this is more common across browsers.

It’s not a huge difference, but it is a very important one.

Link copied to clipboard