Control Scope in Sass Components with BEM


When working with CSS and Sass, controlling scope isn’t always top of mind. While we typically rely on the ampersand (&) for nesting selectors, this approach can quickly become confusing and lead to bloated, hard-to-manage CSS.

Interestingly, JavaScript developers have long solved a similar scope issue by assigning the value of this to a variable called self. Could we use this technique to manage scope more effectively in Sass? Absolutely!

A JavaScript Primer: Understanding Scope with self

In JavaScript, we often assign this to a variable like self to maintain a predictable context, especially in closures, event listeners, or asynchronous functions.

Here’s an example:

function foo() {
  let self = this;

  return function() {
    // self is always `foo`, even inside this closure
    return self;
  }
}

let bar = new foo();

Let’s make this clearer with an event listener example. Given this HTML markup:

<div class="component">
  <div class="component__child-element"></div>
</div>
<div class="component">
  <div class="component__child-element"></div>
</div>

Using JavaScript without self, we have this issue:

function foo() {
  let childElements = [...document.querySelectorAll('.component__child-element')];

  childElements.forEach(element => {
    element.addEventListener('click', function(evt) {
      // `this` refers to the clicked element
      console.log(this);
    });
  });
}

let bar = new foo();

Clicking an element logs the clicked element itself, not the instance of foo.

Using self to store context:

function foo() {
  let self = this;

  let childElements = [...document.querySelectorAll('.component__child-element')];

  childElements.forEach(element => {
    element.addEventListener('click', function(evt) {
      // `self` correctly refers to the instance of foo
      console.log(self);
    });
  });
}

let bar = new foo();

Now, clicking the element logs the correct context (foo), showcasing effective scope management.

How Does This Relate to Sass?

Let’s transfer this concept to Sass and manage CSS scope similarly with a $self variable.

Given this markup:

<div class="component">
  <div class="component__child-element"></div>
</div>
<div class="component component--reversed">
  <div class="component__child-element"></div>
</div>

We have some basic Sass:

.component {
  display: block;
  max-width: 30rem;
  min-height: 30rem;
  margin: 5rem auto;
  background: rebeccapurple;
  position: relative;
  border: 1px dashed rebeccapurple;

  &__child-element {
    width: 15rem;
    height: 15rem;
    border-radius: 50%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
    background: white;
  }
}

We now want to add a reversed modifier that swaps colors. Intuitively, you might try:

.component {
  &--reversed {
    background: white;
    border-color: lightgray;

    &__child-element {
      background: rebeccapurple;
    }
  }
}

But this doesn’t work! The nested &__child-element compiles incorrectly to .component--reversed__child-element.

Using $self to Control Scope

Let’s solve this by setting our root context to a variable, $self:

.component {
  $self: &; // This is now `.component`

  display: block;
  max-width: 30rem;
  min-height: 30rem;
  // other styles...

  &--reversed {
    background: white;
    border-color: lightgray;

    // Using $self to correctly scope the child element
    #{$self}__child-element {
      background: rebeccapurple;
    }
  }
}

This now compiles correctly to:

.component--reversed .component__child-element {
  background: rebeccapurple;
}

Taking $self Further: Cleaner BEM Structure

One practice I enjoy is referencing modifiers directly within the element declaration, grouping styles neatly. For example:

.component {
  $self: &;

  &__child-element {
    width: 15rem;
    height: 15rem;
    background: white;

    // Modify element when parent is reversed
    #{$self}--reversed & {
      background: rebeccapurple;
    }
  }
}

The selector compiles exactly how we want it:

.component--reversed .component__child-element {
  background: rebeccapurple;
}

An Advanced Example: Sibling Selectors

Let’s say we have multiple grids. We want each subsequent grid’s items styled differently:

.grid {
  $self: &;

  &__item {
    // Default styles...

    // Change item color for sibling grids
    #{$self} + #{$self} & {
      background: rebeccapurple;
    }
  }
}

The compiled CSS is powerful and clean:

.grid + .grid .grid__item {
  background: rebeccapurple;
}

Using $self this way ensures flexibility and clarity even in complex scenarios.

Wrapping Up

By borrowing a page from JavaScript’s playbook, we’ve effectively managed scope in Sass. Setting $self: & at the root of your component gives you more control over your selectors, resulting in cleaner, more maintainable CSS. Give it a try—it could revolutionize how you structure your BEM components.