Using Sass to Control Scope With BEM Naming

Avatar of Andy Bell
Andy Bell on

Controlling scope is something you probably don’t tend to consider when working with CSS and Sass. We have had access to the ampersand (&) for quite some time now, which gives us a level of scope—but it’s easy for it to lose its usefulness when you’re nested quite deeply. The & can send us down a windy road of doom when we completely lose track of what it means and can be responsible for some really heavy bloat.

Meanwhile, in JavaScript, we can control the scope of this by casting it as a variable at the point where it means what we will want it to. This is often at the start of a method or object:

function foo() {
  let self = this;

  return function() {
            
    // Self = foo(), even int his closure
    return self;
  }
}
    
// We need to instantiate foo() or its 'this' will be the window
let bar = new foo();

Now that we have self in the above context, we always have a reference to foo() regardless of where we might find ourselves within the function. This is really useful when we end up in setTimeout scenarios, closures or event handlers.

A quick example of an event will hopefully help to illustrate my point.

Using this markup:

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

We can add this JavaScript:

function foo() {

  // Load up all component child elements
  let childElements = [...document.querySelectorAll('.component__child-element')];
   
  // Loop each element and add an event listener
  childElements.map(element => {

      element.addEventListener('click', function(evt) {
          
        // Log what `this` currently is
        console.log(this);
      });
  });
}

// Create a new instance of foo
let bar = new foo();

In that code sample, if you click a component__child-element with your console open, this will report itself as the event target, which happens to be the element that we clicked. This isn’t ideal if you thought it would reference foo().

Now, if we run the same sample with self in place of this in the event handler, the console will report an instance of foo() instead:

function foo() {
    
  // Control scope of this by storing it
  let self = this;
  
  // Load up all component child elements
  let childElements = [...document.querySelectorAll('.component__child-element')];
 
  // Loop each element and add an event listener
  childElements.map(element => {
    element.addEventListener('click', function(evt) {
      
      // Self will report itself as `foo()`
      console.log(self);
    });
  });
}

// Create a new instance of foo
let bar = new foo();

So, how does this relate to Sass?

I’m glad you stuck with me there, because that JavaScript example was a primer for what we’re going to look at with Sass.

First, let’s start with that same markup and some core styles.

<div class="component">
  <div class="component__child-element"></div>
</div>
<div class="component">
  <div class="component__child-element"></div>
</div>
.component {
  display: block;
  max-width: 30rem;
  min-height: 30rem;
  margin: 5rem auto;
  background: rebeccapurple;
  position: relative;
  border: 1px dashed rebeccapurple;
    
  &__child-element {
    display: block;
    width: 15rem;
    height: 15rem;
    border-radius: 50%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
    background: white;
  }
}

This gives us a nice purple square with a white circle inside it. Nothing groundbreaking, but useful for this example.

See the Pen Square with Circle by Geoff Graham (@geoffgraham) on CodePen.

You’ll probably notice that we’re using BEM syntax, so let’s go ahead and create a modifier.

We want an inverted version of .component that has a white background with a purple circle in it. So, let’s go ahead and approach it how we’d probably think to do it straight away by including it in the same nested ruleset:

.component {
  // Other styles redacted for brevity 
  
  // The reversed modifier flips the colors around
  &--reversed {
    background: white;
    border-color: lightgray;
    
    &__child-element {
      background: rebeccapurple;
    }
  }
}

See the Pen Square with Circle by Geoff Graham (@geoffgraham) on CodePen.

Wait, why is this not working? The problem is that the & has a scope of .component--reversed, so &__child-element compiles to .component--reversed__child-element, which doesn’t exists in the markup.

$self to the rescue!

Just like in the JavaScript we looked at before, we’re going to cast our initial scope into a variable called $self. You can do it like this:

.component {
  $self: &;
}

That means that wherever we use $self in our component, it will compile to .component.

So let’s refactor that reversed modifier, so that it actually works:

.component {
  $self: &; // Hey look, it's our new best friend!
  display: block;
  max-width: 30rem;
  min-height: 30rem;
  // Other styles redacted
  
  &--reversed {
    background: white;
    border-color: lightgray;
    
    // Here, we use $self to get the correct scope
    #{ $self }__child-element {
      background: rebeccapurple;
    }
  }
}

The compiled CSS for that element is now .component--reversed .component__child-element which of course exists and successfully turns the inner circle purple:

See the Pen ‘Using Sass to Control Scope With BEM Naming’ example by Andy Bell (@hankchizljaw) on CodePen.

Further exploration

One thing I like to do in my components is reference a parent’s modifier within the element itself, rather than referencing the element within the modifier like we did earlier. This is to keep my styles grouped per element. For the same reason, I’ll also put media queries within elements too.

I end up with code that looks like this:

// We're still within .component where $self has been declared.
&__child-element {
  display: block;
  width: 15rem;
  height: 15rem;
  border-radius: 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate3d(-50%, -50%, 0);
  background: white;
  
  // Flip the color when the parent is reversed
  #{ $self }--reversed & {
    background: rebeccapurple;
  }
}

Having access to $self as the root context gives us the flexibility to approach our modifiers like that with ease. Because $self is .component and & is .component__element, adding the --reversed portion generates us .component--reversed .component__element which is exactly what we wanted.

See the Pen ‘Using Sass to Control Scope With BEM Naming’ example by Andy Bell (@hankchizljaw) on CodePen.

A more advanced example

Now let’s really push the boat out and write something interesting. We’re going to layout three separate grids. Using $self, I’m going to change the color of the grid items using a sibling selector within a grid-item itself.

See the Pen ‘Using Sass to Control Scope With BEM Naming’ example by Andy Bell (@hankchizljaw) on CodePen.

Let’s look at the Sass that affects the color:

&__item {
  // Item styles redacted for brevity

  // For each direct `.grid sibling`, make this item rebeccapurple
  #{ $self } + #{ $self } & {
    background: rebeccapurple;
  }
}

What that selector compiles to is .grid + .grid .grid__item. The combination of $self and & enables us to generate that within the context of the element which is our &. This is incredibly powerful for maintaining some sort of order within complex codebases.

Wrapping up

We’ve taken inspiration from JavaScript to control scope in our BEM components with Sass by using this little snippet at the root: $self: &. With the scope control, we gain flexibility to write clean, organized, BEM driven CSS when we are deep in a modifier or child element.

I hope this little tip will help you to improve your Sass. If it does, get in touch with me on Twitter or drop a comment below. It’ll be great to see how gaining control of scope with Sass has helped you out.