dark mode

Add class to the most outer selector using Sass mixin

Add class to the most outer selector using Sass mixin

Here are the two HTML examples. The difference is the additional bold class.

<!-- example 1 -->
<div class="parent">
  <div class="child"><span>Hello World<span></div>
</div>

<!-- example 2 -->
<div class="parent bold">
  <div class="child"><span>Hello World<span></div>
</div>

How to apply styles to our Hello World only when a bold class is present?

Add a separate CSS rule

.bold span {
  font-weight: bold;
}

/* or more specific */
.bold.parent .child span {
  font-weight: bold;
}

Although this will work, we separate element styling which will not scale well in the long term.

Encapsulate the CSS rule with the root selector

.parent {
  // lines of rules
  span {
  }
  // lines of rules
  .child {
  }
  // lines of rules
  &.bold .child span {
    font-weight: bold;
  }
}

This will work as well and it's better than the previous example, but still will have scaling issues if we style large component with multiple rules.

Using Sass's @at-root rule

.parent {
  // some CSS rules
  .child {
    // some CSS rules
    span {
      // some CSS rules
      @at-root .bold {
        font-weight: bold;
      }
    }
  }
}

This will compile to .bold .parent .child span and won't help us.

It is also unpractical to hardcore selector with @at-root like this:

.parent {
  // some CSS rules
  .child {
    // some CSS rules
    span {
      // some CSS rules
      @at-root .parent.bold .child span {
        font-weight: bold;
      }
    }
  }
}

Solution with mixins

Here is our new most-outer-selector mixin:

@mixin most-outer-selector($new-class) {
  $current-selector: &;
  $new-selector: [];

  @each $item in $current-selector {
    $first-item: nth($item, 1);

    $appended-item: $first-item + $new-class;

    $new-item: set-nth($item, 1, $appended-item);
    $new-selector: append($new-item, $new-selector);
  }

  @at-root #{$new-selector} {
    @content;
  }
}

.parent {
  // some CSS rules
  .child {
    // some CSS rules
    span {
      // some CSS rules
      @include most-outer-selector('.bold') {
        font-weight: bold;
      }
    }
  }
}

This will compile CSS selector to .parent.bold .child span.

How it works

We use our mixin within the span element. This way we will place our styles where it should be.

To get the nested selector .parent .child span we have to use Parent Selector.

From the SCSS documentation: When a parent selector is used in an inner selector, it’s replaced with the corresponding outer selector.

This is what the line $current-selector: &; is doing. We fetch the whole selector.

Next we initialise an empty array to the new variable $new-selector which will be our new selector at the end.

In the @each block we loop through our $current-selector. In our case, the array will have only one item. To have more items, our CSS must contain selectors with comma like .child, .child2.

Inside the loop, the $item will be .parent .child span. To get the .parent class we use the list method nth which takes the array, and the wanted position. In our case we want to loop through $item and get the first item.

Then we concatenate this first item with our new class, the variable $appended-item will be .parent.bold.

To add the rest of the classes, we use set-nth method which takes the array which index will replace (lists/arrays in SCSS start from 1) and the newly created variable $appended-item. Now we have the wanted selector as $new-item variable.

We put the variable inside of the $new-selector, because as we pointed before the $current-selector might contain many CSS selectors.

Finally, we can create the CSS styles with @at-root and the new selector. The @content adds all styles inside the @include block.

Related articles

© 2020 All rights reserved.