Break Out of CSS Nesting with Sass

Abstract image used to represent Break Out of CSS Nesting with Sass
Image by Bill Jelen.

Using Sass to write CSS following methodologies like BEM is incredibly easy by utilising the parent selector. However, what this can also lead to especially with a few levels of nested selectors is a complex structure where you find yourself needing to 'break out' of the CSS nesting at that point in the stylesheet and emit a ruleset outside its parent selectors.

To answer this problem, enter @at-root!

In simplest terms, @at-root is a Sass atrule that emits its nested style rules without the surrounding selector nesting. It changes the generated stylesheet, not where an element sits in the DOM. By default it preserves enclosing atrules such as media queries.

For example:

.classname {
  color: black;

  .child {
    color: lime;
    @at-root {
      .child {
        color: pink;
      }
    }
  }
}

// Generated CSS:
.classname {
  color: black;
}

.classname .child {
  color: lime;
}

.child {
  color: pink;
}

You can use a block, as above, with an explicit selector inside it. Alternatively, put the selector directly after @at-root to emit that rule outside the surrounding selector nesting.

The code below will achieve the same output CSS as above:

.classname {
  color: black;

  .child {
    color: lime;
  }

  @at-root .child {
    color: pink;
  }
}

BEM helps keep selectors independent of the surrounding markup. Browsers generally match selectors from right to left, but selector length alone does not tell us whether a stylesheet is slow. Profile the page before treating this as a performance problem. Here, the useful part is controlling which ancestor selectors appear in the output.

For example, I like to use @at-root within mixins when an element needs different styling depending on a classname on the body. This mixin emits body.state followed by the current element selector, without carrying its other surrounding selectors into that rule:

// Mixin
@mixin whenState($el) {
  @at-root body.state {
    #{$el} {
      @content;
    }
  }
}

// Use in Sass
.element {
  color: lime;

  &__child {
    color: purple;

    @include whenState(&) {
      color: pink;
    }
  }
}

// Generated CSS:
.element {
  color: lime;
}

.element__child {
  color: purple;
}

body.state .element__child {
  color: pink;
}

In this way, you can retain your BEM structure and organisation whilst also breaking out of the generated CSS at the opportune point where a single element needs different styling depending on a classname further up the DOM. It's exactly how I implemented dark mode on my website.

See here for the Sass documentation on @at-root and a few more use cases and examples.


Planning a platform change?

I help teams make difficult platform work clearer, from architecture decisions and migrations to launch recovery, performance, and search visibility.