Sass Extends: be aware of the loop

by Dale Sande

I have been promoting the use of Sass' @extend function for some time now so it is safe to say that I have been very cautious of how this feature processes Sass into CSS. I still stand behind this feature and it is extremely powerful, but with power comes responsibility.

In a recent project I needed to apply some basic styling to some heading tags, we have all seen something like the following.

h1 {
  @include heading();
}

h2 {
  @include text($heading_2);
}

h3 {
  @include text($heading_3);
}

Pretty textbook stuff. Each line of code there is applying a specific style to the the selector. We would expect the following CSS output from our Sass.

CSS

h1 {
  font-size: 3.83333em;
  line-height: 1.17391em;
  margin-bottom: 0.3913em;
  color: #333333;
  font-weight: normal;
  font-family: "Helvetica Neue", Arial, sans-serif;
  text-transform: uppercase; }

h2 {
  font-size: 2.66667em;
  line-height: 1.125em;
  margin-bottom: 0.5625em; }

h3 {
  font-size: 2.33333em;
  line-height: 1.28571em;
  margin-bottom: 0.64286em; }

Then things get a little more interesting. For the h1 you want to do some text transformation, so you apply text-transform: uppercase; decloration to the h1 like so.

h1 {
  @include heading();
  text-transform: uppercase;
}

What about h2 and h3? We could either specifically add the text-transform: uppercase; to each selector, but we want to use Sass' @extend feature to extend the CSS rules from the h1 as well, so we update the code as shown in the following example.

h1 {
  @include heading();
  text-transform: uppercase;
}

h2 {
  @include text($heading_2);
  @extend h1;
}

h3 {
  @include text($heading_3);
  @extend h1;
}

This produces the following CSS as expected. Notice where we have h1, h2, h3 properly extended.

CSS

h1, h2, h3 {
  font-size: 3.83333em;
  line-height: 1.17391em;
  margin-bottom: 0.3913em;
  color: #333333;
  font-weight: normal;
  font-family: "Helvetica Neue", Arial, sans-serif;
  text-transform: uppercase; }

h2 {
  font-size: 2.66667em;
  line-height: 1.125em;
  margin-bottom: 0.5625em; }

h3 {
  font-size: 2.33333em;
  line-height: 1.28571em;
  margin-bottom: 0.64286em; }

At this point we are feeling pretty awesome and this is all working as expected. As we begin to build a module for our UI, there comes a point where we need to make some color modifications to the heading styles.

In our example, headings are inheriting it's color from the <html> tag which is #333. The new module has a transparent background-color based off of the #333 color, something like background-color: transparentize($color, 0.5); or background-color: rgba(51, 51, 51, 0.5); for the CSS kids in the room.

Design solution - make the text white. Starting with the h1 we would write the following code.

.module {
  background-color: transparentize($color, 0.5);
  h1 {
    color: $white;
  }
}

Perfect, refresh the browser. Wait, we notice something unexpected. The h1, h2 and h3 are all white? What? But we only specified the h1, why did this happen? Opening the inspector we see that .module h1, .module h2, .module h3 have all been extended and the declaration of color: white; applied as seen in the following example.

CSS

.module {
  background-color: rgba(51, 51, 51, 0.5); }
  .module h1, .module h2, .module h3 {
    color: white; }

Looking at the Sass docs we see the following statement, @extend works by inserting the extending selector (e.g. .seriousError) anywhere in the stylesheet that the extended selector (.e.g .error) appears. What does that really mean?

Using the examples in the Sass docs, let's do something like the following. In the same way that we used heading selectors to create a primary selector that is extended into the secondary selector, this example uses simple CSS classes.

.error {
  color: error;
}
.seriousError {
  @extend .error;
}

To mix things up we crate a new .new-block selector and nest inside a new named-spaced CSS rule for .error.

.new-block {
  .error {
    color: nested-error;
  }
}

And yup, as expected, not only was .error extended with .seriousError, but the nested version of .error was extended as well.

CSS

.error, .seriousError {
  color: error; }

.new-block .error, .new-block .seriousError {
  color: nested-error; }

I am reminded by the following once again

@extend works by inserting the extending selector (e.g. .seriousError) anywhere in the stylesheet that the extended selector (.e.g .error) appears.

The 'ah-ha' moment

Having run this concept through multiple scenarios in SassMeister I continue to come to the same simple conclusion. Be aware of the loop. It's really that simple.

Taking the previous heading example, if we were to of used an h2 nested within the .module class, none of this would have happened. The h2 is extending h1, but nothing is yet extending h2, so the loop is short and you get what you would expect in the following example.

.module h2 {
  color: white;
}

Our h1 is extended by the h2 and the h3. As a result any re-introduction of a new h1 style rule will result in the full cascade of extended selectors.

Just doing this …

h1 {
  foo: bar;
}

Will give you this …

h1, h2, h3 {
  foo: bar;
}

And if you were to do something like this …

.super {
  .cali {
    .fragi {
      .listic {
        h1 {
          border-width: 1px; 
        }
      } 
    }
  }
}

Yup, you'd get this …

.super .cali .fragi .listic h1, .super .cali .fragi .listic h2, .super .cali .fragi .listic h3 {
  border-width: 1px;
}       

SassMeister.com

Play with the SassMeister Living Gist