
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.