Beware of Selector Nesting in Sass

Share this article

A couple of days ago, I tweeted about how I thought selector nesting actually caused more problems than it solved.

Some people agreed, some did not. In any case, it raised a couple of interesting thoughts so I thought I would put together an article to talk about the topic.

What is selector nesting?

Selector nesting is a feature of CSS preprocessors, allowing authors to nest selectors within selectors in order to create shortcuts. For instance:

.parent {
  color: red;

  .child {
    color: blue;
  }
}

Which would compile to the following CSS:

.parent {
  color: red;
}

.parent .child {
  color: blue;
}

In this example, .child is nested within .parent in order to avoid repeating the parent selector.

While this can be quite useful, I feel like this feature is being largely overused to the extent that we now try to solve issues we inflicted ourselves while nesting too much.

What’s wrong with nesting?

In itself, there’s absolutely nothing wrong with nesting. The feature makes sense. The problem, as is often the case, is not the feature but how we use it. Allow me to start with a couple of examples I stumbled upon.

There is this one from Micah Godbolt:

.tabs {
  .tab {
    background: red;

    &:hover {
      background: white;
    }

    .tab-link {
      color: white;

      @at-root #{selector-replace(&, '.tab', '.tab:hover')} {
        color: red;
      }
    }
  }
}

Or this one from ZI Qiu:

.root {
  width: 400px;
  margin: 0 auto;

  .links {
    .link {
      display: inline-block;

      & ~ .link {
        margin-left: 10px;
      }

      a {
        padding: 10px 40px;
        cursor: pointer;
        background: gray;

        &:hover {
          background: blue;
          color: white;
          font-size: 700;
        }

        .icon {
          margin-right: 5px;
          @include selector-modifier(-2 ':hover', 1 suffix '.zh') {
            color: red;
            background: green;
          }
          @include selector-modifier(-2 ':hover', 1 suffix '.en') {
            color: yellow;
            background: green;
          }
        }
      }
    }
  }
}

Let’s start with a disclaimer: This is clever code. In no way am I saying that this is bad code. I assume this code is doing exactly what it is intended to do.

Now, what if I asked you what exactly are those two examples meant to do? Base on a first glance, without spending a couple of minutes looking at the code, would you be able to guess?

Me neither. Because it is complicated.

Nesting makes code complicated

Both of the examples shown above use some selector functions (from Sass 3.4) to partially rewrite the current selector context (&).

So if I’m not mistaken, both examples involve more code in order to write less code, all of this in addition to some extra complexity. What about writing some simple code from the start?

This is a statement I already made before: Selector functions are not meant for everyday use. I believe Chris Eppstein and Natalie Weizenbaum explicitly said they were essentially adding this feature to help framework developers.

Note: if you find a legit use case for selector functions, that actually solves a problem, please be sure to show me, as I’d be interested in taking a look at that.

The Reference selector is ambiguous

The reference selector in Sass (&) can sometimes be quite ambiguous. Depending on the way it is used, it can yield a totally different CSS output. Here is a collection of simple examples.

/* SCSS */
.element {
  &:hover {
    color: red;
  }
}

/* CSS */
.element:hover {
  color: red;
}
/* SCSS */
.element {
  &hover {
    color: red;
  }
}

/* CSS */
.elementhover {
  color: red;
}
/* SCSS */
.element {
  & .hover {
    color: red;
  }
}

/* CSS */
.element .hover {
  color: red;
}
/* SCSS */
.element {
  &-hover {
    color: red;
  }
}

/* CSS */
.element-hover {
  color: red;
}
/* SCSS */
.element {
  &.hover {
    color: red;
  }
}

/* CSS */
.element.hover {
  color: red;
}
/* SCSS */
.element {
  .hover& {
    color: red;
  }
}

/* Syntax Error */
Invalid CSS after ".hover": expected "{", was "&"

"&" may only be used at the beginning of a compound selector.
/* SCSS */
.element {
  &:hover & {
    color: red;
  }
}

/* CSS */
.element:hover .element {
  color: red;
}
/* SCSS */
.element {
  &:hover {
    & {
      color: red;
    }
  }
}

/* CSS */
.element:hover {
  color: red;
}

And we are keeping things simple here with a single selector. Needless to say, it can get quite complicated when you multiply the references in a single rule.

The thing is, some operations work, some don’t (notice the error displayed in one of the examples above). Some generate a compound selector, some don’t. Depending on what’s being done and the Sass background of the next developer to use the code, it can become quite difficult to debug this kind of thing.

Unsearchable selectors

At this point I am mostly nitpicking. But there is something I don’t like that involves Sass code extensively using nesting to build BEM-like selectors:

.block {
  /* Some CSS declarations */

  &--modifier {
    /* Some CSS declarations for the modifier */
  }

  &__element {
    /* Some CSS for the element */

    &--modifier {
      /* Some CSS for the modifier of the element */
    }
  }
}

Before moving on to why I don’t like this, let me remind you what this code will compile to:

.block {
  /* Some CSS declarations */
}

.block--modifier {
  /* Some CSS declarations for the modifier */
}

.block__element {
  /* Some CSS for the element */
}

.block__element--modifier {
  /* Some CSS for the modifier of the element */
}

On one hand, this avoids repeating .block in each selector, which can be a pain when you have block names like .user-profile.

On the other hand, it creates new selectors out of nowhere that are thus impossible to search for. What if a developer wants to find the CSS from .block__element? Chances are high he’ll try to search it in the project from his IDE, finding nothing because this selector has never been authored as is.

Actually, it looks like I’m not the only one to hold this view. Kaelig, who was working at The Guardian at the time, tweeted this:

Also, I think it’s nicer to have the base name repeated. This way, it is crystal clear what’s going on.

I should note that source maps can help with this to some extent, but they don’t change the fact that the code base itself is still unsearchable.

When is it okay to use nesting?

As long as you and your team feel okay with the extra complexity it can involve, it is always okay to use selector nesting. If everybody’s fine with it, then that’s awesome!

If you ask me now, I feel like adding pseudo-classes and pseudo-elements are pretty much the only case where it’s fine. For instance:

.element {
  /* Some CSS declarations */

  &:hover,
  &:focus {
    /* More CSS declarations for hover/focus state */
  }

  &::before {
    /* Some CSS declarations for before pseudo-element */
  }
}

This is the best use case for selector nesting I can think of. Not only does it avoid repeating the same selector, but it also scopes everything from this element (states and virtual children) in the same CSS rule set. Also, & is not ambiguous: it means .element, no more, no less.

Another case where I do think nesting is interesting is when you want to apply some custom styles to a simple selector based on the upper context. For instance, when using Modernizr’s CSS hooks:

.element {
  /* Some CSS declarations */

  .no-csstransforms & {
    /* Some CSS declarations when CSS transforms are not supported */
  }
}

In this scenario, I feel like this is clear enough that .no-csstransforms & is intended to contextualize the current selector whenever CSS transforms are not supported. Although, this could be on the borderline of what I would consider acceptable use.

What are my recommendations?

Write simple CSS. Let’s take Micah’s example, which is slightly complicated, but not so complicated that it would be a pain to re-write.

This is the current code, obviously meant to style some tabs:

.tabs {
  overflow: hidden;

  .tab {
    background: red;

    &:hover {
      background: white;
    }

    .tab-link {
      color: white;

      @at-root #{selector-replace(&, '.tab', '.tab:hover')} {
        color: red;
      }
    }
  }
}

We could instead write it like this:

.tabs {
  overflow: hidden;
}

.tab {
  background: red;

  &:hover {
    background: white;
  }
}

.tab-link {
  color: white;

  .tab:hover & {
    color: red;
  }
}

I can’t think of a single reason for someone to prefer the first code snippet. Not only is it longer, but it’s also less explicit and makes use of Sass features that are not necessarily known by all developers.

Note that the CSS output is not exactly the same because we also simplified the code. Rather than having selectors as long as .tabs .tab .tab-link, we moved to easier things like .tab-link.

At this point, it’s not so much selector nesting but naming conventions and selector methodology. When using BEM, for instance, you name things after what they are rather than where they are, which often results in simple selectors (here simple means not compound), meaning there is no nesting involved.

According to CSS Guidelines from Harry Roberts:

It is important when writing CSS that we scope our selectors correctly, and that we’re selecting the right things for the right reasons […] Given the ever-changing nature of most UI projects, and the move to more component-based architectures, it is in our interests not to style things based on where they are, but on what they are.

A good rule of thumb when it comes to CSS selectors is the shorter the better. Not only is is better at any level (performance, simplicity, portability, intent, etc.), but it turns out it’s much easier not to screw things up with nesting when selectors are short.

Final thoughts

Whenever I say how I think selector nesting isn’t such a good idea, people always tell me “I never had an issue”, “we use it every day, no problem”, “it’s because you’re doing crazy things with it”. Of course there is nothing wrong with the feature.

Preprocessors don’t output bad code, bad developers do. And here, it’s not even about the output but about the input. Code becomes less and less readable when we add extra layers of complexity. Nesting selectors is one of those layers.

We were given nesting, so we abused its power. Then we were given selector functions, so we used those in order to fix the mess we made with nesting in the first place. This is wrong.

Use Sass, or any preprocessor for that matter, to simplify your code, not to make it more complex.

This article has been translated into French on La Cascade. Merci, Pierre!

Frequently Asked Questions (FAQs) about Selector Nesting in SASS

What is the significance of selector nesting in SASS?

Selector nesting in SASS is a powerful feature that allows you to write more maintainable and readable CSS. It helps in organizing your CSS code in a way that follows the same visual hierarchy as your HTML. With selector nesting, you can define styles in a hierarchical manner, which can make your code cleaner and easier to understand. However, it’s important to use this feature wisely as over-nesting can lead to unnecessary complexity and specificity issues.

How can I avoid over-nesting in SASS?

Over-nesting can lead to bloated CSS and specificity issues. To avoid this, try to limit your nesting to 3 levels deep. If you find yourself going deeper, consider breaking your rules into smaller, reusable chunks. Also, make use of the parent selector (&) wisely. It can help you control the scope of your styles and prevent unnecessary nesting.

What is the role of the parent selector (&) in SASS?

The parent selector (&) in SASS is used to reference the parent of a nested rule. It can be incredibly useful for pseudo-classes like :hover or :focus. However, it can also be used to create more complex selectors, or to append classes or IDs to the parent selector. It’s a powerful tool, but like all powerful tools, it should be used with caution to avoid creating overly complex selectors.

How does selector nesting affect CSS specificity?

Selector nesting can increase the specificity of your CSS rules. Each level of nesting adds to the specificity, meaning nested selectors will override any conflicting styles defined elsewhere. This can be both a blessing and a curse. While it can help you target elements more precisely, it can also lead to specificity wars, where you find yourself adding more and more selectors just to override existing styles.

Can I use selector nesting with media queries in SASS?

Yes, you can use selector nesting within media queries in SASS. This can be a powerful way to organize your responsive styles, as it allows you to keep the styles for each component together, rather than having separate sections for each breakpoint. However, as with all uses of nesting, it’s important to be mindful of the potential for increased specificity and complexity.

How can I refactor my CSS to make better use of SASS nesting?

Refactoring CSS for SASS nesting involves identifying patterns and redundancies in your CSS, and then reorganizing your rules to take advantage of SASS’s hierarchical syntax. This might involve grouping related styles together, breaking complex rules into smaller, reusable mixins, or using variables to manage common values.

What are the best practices for using SASS nesting?

Some best practices for using SASS nesting include: limiting your nesting to 3 levels deep, using the parent selector (&) wisely, avoiding overuse of nesting with complex selectors, and being mindful of the impact on CSS specificity. It’s also a good idea to regularly review and refactor your SASS to keep it clean and maintainable.

How does SASS nesting compare to CSS nesting?

While CSS does support some level of nesting via the use of descendant selectors, SASS takes this concept much further. SASS allows for much deeper nesting, and provides additional features like the parent selector (&) and the ability to nest media queries. However, it’s important to remember that all SASS nesting ultimately compiles down to CSS, so the same principles of good CSS architecture still apply.

Can I use SASS nesting with other preprocessor features like mixins and variables?

Yes, SASS nesting can be used in conjunction with other preprocessor features like mixins and variables. This can be a powerful way to create reusable, modular CSS. For example, you might define a mixin for a common pattern, and then use nesting to apply that pattern in different contexts.

What are the potential pitfalls of SASS nesting?

While SASS nesting can make your CSS more organized and maintainable, it can also lead to potential pitfalls. Over-nesting can result in bloated, hard-to-maintain CSS. Overuse of the parent selector (&) can create overly complex selectors. And nesting can increase the specificity of your CSS, leading to potential conflicts and overrides. As with any tool, it’s important to use SASS nesting wisely and responsibly.

Kitty GiraudelKitty Giraudel
View Author

Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/they.

LouisLsassselector nesting
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week