Deeply nested SCSS is good.

Evgenia Karunus
8 min readSep 3, 2019

--

Deep SCSS nesting is unanimously considered a bad practice, and I want to discuss it here (at the very least in order to avoid writing it all over again, separately, in a private chat, to each of my coworkers).

I believe that deeply nested CSS is not a bad practice, in fact I believe it to be an actively good practice. Not the ‘it’s fine if we have a few nested selectors if we really need it!’, the ‘please strictly nest your selectors, using the direct parent > selector’.

Deep, strict SCSS nesting allows for encapsulation, lets us reason more easily about our CSS code, and makes it significantly more readable.

It’s claimed that deep nesting introduces unneeded specificity, it’s less performant, it’s too tightly coupled to HTML, or outright not readable. In this opinion piece, I’d like to refute these claims, one by one.

Most popular arguments against nested CSS

Let’s list the most popular reasons for avoiding nested CSS, and my response to these reasons.

1. Claim: Nesting creates high specificity, which is harder to override

This is true, locally scoped CSS is harder to override from the outside. But here is an idea: you should avoid overriding your CSS anyway. What if overriding something in your code indicates that you should have structured it some other way? What if making overriding easy just sweeps this fact under the carpet?

Here are 2 situations when you need to override your CSS you can stumble upon (when you use high specificity selectors in particular), and why both situations are fine:

  1. If you are overriding a small amount of css rules, — then high specificity shouldn’t be a significant issue for you, just throw in 3 !importants in your css, that won’t make it worse.
  2. If you are overriding a lot of css rules, — then something went wrong!
    - If you are overriding your own css, — it means your css was NOT SPECIFIC ENOUGH at some point. It means you are overriding something that should have been scoped under a more specific rule.
    - If you are overriding (a lot of) css of some library you use, — it means that that library should have provided you with some means of not including their css at all. This can be achieved by, for example, passing your own classNames for particular components of said library, if you don’t need default styles for those particular components.

Avoiding high specificity in CSS is akin to avoiding locally scoped variables. It’s true that we can’t reuse our .foobar somewhere higher up the scope without explicitly moving it up the scope. But it means that our namespace is not littered with unused variables, and it means that we won’t have the need for overrides if we structure our CSS well. Don’t override, refactor:

The methodology is to nest as little as necessary and generalize judiciously. So you don’t override a style, you refactor. If we find that a rule is too specific and it needs to apply more broadly, it gets moved up the tree, not overridden or redefined. If we find that a rule is too specific and it needs to apply more broadly, it gets moved up the tree, not overridden or redefined.

2. Claim: CSS resulting from deep nesting harms performance

There are two concerns regarding the performance of deeply-nested CSS —
1. Files resulting from deep nesting turn out bigger, and
2. Css rules in the form of header > ul > li > a slow down CSS rendering.

The first concern is based on the fact that this scss, for example:

header > .container {
background: red;
> a.logo {
border-radius: 5px;
}
> nav {
color: blue;
}
}

will expand into this css:

header > .container { background: red; }
header > .container > a.logo { border-radius: 5px; }
header > .container > nav { color: blue; }

which has a lot of repetition in it, and it means that the .css file we’ll be sending over the wire will be significantly larger than if we were to avoid nesting.
However, if you care about performance, all of your css files are probably already getting gzipped. Compression in gzip is achieved primarily via the matching and replacement of duplicate strings.
We should not worry about it more than we worry about file size when we use, for example, BEM methodology, with its 26-symbol classnames (.uppy-dashboardItem-preview).

The second concern is based on the fact that the fastest selectors to find for CSSDom are id-based, and class-based. The difference between the nested selector (e.g. .box > .title) and class selector (e.g. .box--title) is 0.8337ms for 50000 elements (which is a very significant amount of elements per page). That’s 0.0008th of a second. For 50000 elements.

Overall, about performance, — I believe performance is frequently scapegoated when we want to prove something in programming.
We use ‘performance’ as an argument because it’s something measurable, something objective, it makes our argument sound substantial.
I hope I dispelled some worries about performance above, but I come at it from a place of, that, as Matthew Jones said, — performance doesn’t matter unless you can prove that it does.

3. Claim: Tight coupling between your CSS and HTML is bad

Another concern I stumbled upon is ‘Any change you make to your markup will need to be reflected into your Sass and vice versa.’ (http://thesassway.com/beginner/the-inception-rule).
My answer to that is — yes, you will have to change your CSS if you change your markup. Many changes to your HTML markup will need to be reflected in your SCSS.

A classic example of when deeply nested CSS requires updates that flat classname-based CSS wouldn’t, is when, in our HTML, we move a child of some parent element out of that parent.

For example, we moved ul out of nav. Then we’d go from:

header {
background: red;
> a.logo {
border-radius: 5px;
}
> nav {
color: blue;
> ul{
list-style-type: none;
}
}
}

to:

header {
background: red;
> a.logo {
border-radius: 5px;
}
> nav {
color: blue;
}
> ul{
list-style-type: none;
}
}

Why would we want to perform this extra step, right? Isn’t it a sign of good code, - when we don’t need to change something just because we changed something else?
Not always. We would want this extra step, because in the real world this is not the CSS you’d have on your site.
In the real world, your CSS is filled with { display: flex; }, with { width: 80%; }, and with { float: left; }, you got the gist. It doesn’t matter what positioning method you use, — you want to know what elements surround you. What is the display value of your siblings, whether they are block-level or inline, what widths and heights they take up, and what position value their parent has.
When I refactor SCSS code (otherwise amazing code!) to use strict nesting, I frequently discover unneeded CSS rules, dangling selectors that don’t exist in HTML anymore (is that the loose coupling we wanted?), and excessive HTML tags. All of this could be easily avoided if the coupling between HTML and CSS was tighter rather than looser.

Nesting can give us an amazing overview of our HTML as Jason Zimdar noted, e.g.:

Keep in mind that in the real world this will not be <html>, or <body>. The highest parent selector will be a local unique className defined for a <section>, usually it will be autogenerated for you (in the standard React+Webpack setup at least, see CSS modules).

HTML and CSS are already tightly coupled, there is no way around it, and it’s better when this coupling is visually apparent in your CSS files.

4. Claim: Readability - do we really want our selectors indented by 10 tabs?

We most certainly don’t want our selectors nested 10 levels deep, and I, of course, advise against nesting your selectors all the way down from the <html> tag.
In modern web development, we divide our application into smaller reusable components, and they usually go along with their own CSS, which is locally scoped for each such component.
This is an amazing idea, and it goes a long way in scoping our CSS, even without stricter nesting. Strict nesting is more useful for readability rather than for namespacing and specificity (even though that’s also important).
In such a setup I only ever needed to go at most 5 levels deep in my selectors, which is more than readable.

Arguments PRO deep SCSS nesting

Now that I, hopefully, addressed your concerns, and showed that nested SCSS is not harder to override, not necessarily less performant, and not less readable, — let’s go over why nesting your CSS is not only not harmful, but is actively good.

  1. High specificity is good. Global variables are bad.
    When you write .firstClass .secondClass{} you are effectively declaring a global variable for all children of the .firstClass element. You can’t name any other class .secondClass in that scope.
    Strict nesting allows your SCSS to be as locally scoped as possible.
    In a carefully structured codebase you will rarely if ever need to override your CSS. It’s a sign that you structured your CSS well!
  2. Your CSS and HTML will be connected. Encapsulating code into functions/classes/files is good.
    There will never be pieces of CSS that we forgot to delete after removing the corresponding html element. We delete one div.class — we delete one piece of css, along with all of its children.
  3. Readability.
    In the age of flexbox, we want the hierarchy of our elements explicitly apparent from reading our CSS.
    When we look at our CSS code, — we want to have a clear picture of HTML structure. Thankfully, nesting allows for that.
    Here is an excerpt from the real codebase, — notice how easy it is to predict the look of this component, just by quickly parsing SCSS.
Strictly nested, locally scoped SCSS

Here are a few links I used for reference:

--

--