DEV Community

loading...

Sassy BEM

uxferanmi profile image Fẹrànmí Akínlàdé ・8 min read

Specificity, scoping, reusable code, code clarity⁠—these are major problems plaguing traditional CSS. If you've ever struggled with making something work in CSS, the reason likely traces back to something on this list. As you would expect, there are many tools in the ecosystem aimed at solving one or more of these issues. One is the popular BEM methodology. Another perhaps more notable tool is the ubiquitous Sass preprocessor. BEM solves the problem of specificity and code clarity very elegantly. Sass is perfect for creating reusable CSS code. And it's nesting feature offers a convenient way to keep styles scoped within a container. I thought it would be great to take advantage of both, but they weren't exactly made for each other. I did some research and came up with a system for structuring my syntactically awesome stylesheets, using BEM the DRY way.

Save the Nest

The first problem I had integrating BEM into SCSS was having to throw away nesting. With BEM, you should only ever use a single class selector for every rule set. Page hierarchy is expressed through the block__element naming convention rather than descendant or child selectors in order to avoid conflicts due to specificity. But I love my nesting, it improves code clarity having your stylesheets visually mimic your HTML structure—if you do it right. What to do?

Ace In The Hole: The Parent Selector

SCSS has a parent selector—the & character—which is used in nested rulesets to refer to the selector of the parent block. It also has an interesting side-effect: it breaks out of the containing block, that is, it doesn't compile down to a descendant selector like normal nested blocks do in Sass. This feature is perfect for when you want to add styles for modified versions of a block, while keeping everything grouped together. Consider the example below.

/**** SCSS */
.button {
  border-radius: 4px;
  box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.5);

  .icon {
    font-size: 1.2em;
  }
}

/**** Compiled CSS */
.button {
  border-radius: 4px;
  box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.5);
}

.button .icon {
  font-size: 1.2em;
}

Now if I wanted to style the :hover state of that button, simply adding a nested rule would create a descendant selector, just like it did for .icon. So I would have to put that in a separate block. Bummer. I would much rather group related styles in the same block.

Thankfully, there's a way to this!

/**** SCSS */
.button {
  border-radius: 4px;
  box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.5);

  &:hover {
    box-shadow: none;
  }

  .icon {
    font-size: 1.2em;
  }
}

/**** Compiled CSS */
.button {
  border-radius: 4px;
  box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.5);
}

.button:hover {
  box-shadow: none;
}

.button .icon {
  font-size: 1.2em;
}

This feature works with special characters so it pairs very well with the BEM naming convention. BEMifying the button component above, the Sass rule would become:

/**** SCSS */
.button {
  border-radius: 4px;
  box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.5);

  &:hover {
    box-shadow: none;
  }

  &__icon {
    font-size: 1.2em;
  }
}

/**** Compiled CSS */
.button {
  border-radius: 4px;
  box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.5);
}

.button:hover {
  box-shadow: none;
}

.button__icon {
  font-size: 1.2em;
}

Awesome!! This solves the most important aspect of using Sass with BEM. And what is more, it is DRYer! You don't have to repeat the parent block name in each child element's selector. Write less, do more.

Tidbit: The special & character also functions as a variable, so it can be used with interpolation. When used in this way, it does not break out of the containing block but produces the usual descendant selector. This could come in handy when you need more specificity to override some global styles, or for some other reason. Ideally, you shouldn't have specificity problems like this if you are using BEM, but we often have to work with constraints so this might just save you some trouble. Here's an example.

// Some global styles
p a {
  color: $accent-color;
}

/**** SCSS */
.paragraph {
  margin: 20px 0;

  // Compiles to a single class selector, not enough specificity.
  &__special-link {
    color: $primary-color;
  }

  // Compiles to a descendant selector, overrides global style.
  #{&}__special-link {
    color: $primary-color;
  }
}

/**** Compiled CSS */
.paragraph {
  margin: 20px 0;
}

// From the usual parent selector
.paragraph__special-link {
  color: $primary-color;
}

// From interpolation
.paragraph .paragraph__special-link {
  color: $primary-color;
}

First-class Modifiers

So we've seen the B and the E in the examples above, what about the M? The standard BEM modifier syntax fits well into the pattern described above. Simply replace the double underscores with double dashes. Easy as pie. But...

I had a little concern. If everything comes down to a single class selector, how do I ensure that the modifier always takes precedence over the base class? By definition, a modifier should, you know, modify the default behaviour of the block or element to which it is applied. So it makes sense that modifiers should have higher specificity, right?

Well, not necessarily. The CSS cascade follows an obey-the-last-order system for rules that have equal specificity. This means that provided the modifier class appears after the base class in the compiled CSS, it should override.

However, this feels a little too brittle to me. I prefer to be more explicit in this regard. I prefer first-class modifiers: modifiers that are guaranteed to always take precedence because they are explicitly declared with higher specificity.

A secondary consideration

While Sass allowed me to declare elements without the repetition inherent in traditional BEM, I didn't have this advantage in JavaScript. Modifiers are a thing that you often have to toggle with JavaScript. I didn't much like the idea of typing out longer class names for each state my elements may be in. It's a little thing, but since I was going to deviate from standard BEM to achieve first-class modifiers, I factored this in as well and came up with something less verbose, and with higher specifity.

is-stateful

A simple prefix. Instead of writing

<p class="paragraph paragraph--with-state">

I find it better to write

<p class="paragraph is-with-state">

Why the prefix? Clarity. At a glance, anyone should be able to tell that this class name does not denote a block, but is merely a state modifier. BEM uses the entire base class to prefix modifiers, but a simple is- prefix does the same thing with less verbosity.

But what about namespace collisions? As you will soon see, this is not an issue because modifiers are scoped to their base class, so two elements with the same state identifier will never conflict with each other.

Here is what the SCSS for the HTML above would look like.

/****** SCSS */
.paragraph {
  font-weight: normal;

  &.is-warning {
    font-weight: bold;
  } 
}

// A second block might have a modifier with the same name.
.dialog {
  font-weight: 500;

  &.is-warning {
    font-weight: bolder;
  } 
}

/**** Compiled CSS */
.paragraph {
  font-weight: normal;
}

.paragraph.is-warning {
  font-weight: bold;
}

// There is no conflict between the two blocks. The modifiers are scoped.
.dialog {
  font-weight: 500;
}

.dialog.is-warning {
  font-weight: bolder;
}

Composition: The Block in a Block

BEM is built around blocks, or to put it differently, components. Composition is where the beauty of components lie. Components are like lego blocks. You can arrange them differently to create widely differing structures. But regardless of the arrangement, each lego block never changes it's appearance. It's the combination of them that produces the difference. Composing with BEM should be the same.

When a block is contained within another block, it could have two classes. The block class, and the element class.

<p class="paragraph is-warning">
  Some warning text. Followed by advice on how to handle the issue. There 
  is a cta button to help carry out this advice in one click.

  <button class="paragraph__cta button is-with-icon">
    <i class="fa fa-wrench"></i>
    Fix automatically
  </button>
</p>

Notice that the <button> has three classes. The modifier is easily identified by the is- prefix. The element class is clearly marked with the double underscore pattern. There is a third class with none of the special conventions previously mentioned. This class denotes the block. These three classes are used together because the button is it's own self-contained block which is also used as an element within a larger block.

Like a lego block, the .button class should handle the composition of the button element. The button component should expose modifier classes for tweaking it's appearance as needed.

And like a lego block, the paragraph__cta class defines how this particular block fits into the rest of the structure. The element class is defined by the containing parent and should handle the positioning of the child component within it's parent block.

To recap, when there is a block within a block, you may end up with multiple classes:

  1. a block class defines the component
  2. an element class positions the component within it's containing block
  3. one or more modifier classes customize the component as needed.

Let's style the nested blocks above to see this in action.

/****** SCSS */
.paragraph {
  text-align: justify;

  &.is-warning {
    font-weight: 500;
  }

  &__cta {
    margin: 25px auto;
  }
}

.button {
  border-radius: 5px;
  padding: 10px;
  background: $primary-color;
  color: white;

  &.is-with-icon {
    display: flex;
    justify-content: space-between;
    align-items: center;

    .fa {
      border-right: 1px solid white;
      padding: 3px;
    }
  }
}

/****** Compiled CSS */
.paragraph {
  text-align: justify;
}

.paragraph.is-warning {
  font-weight: 500;
}

.paragraph__cta {
  margin: 25px auto;
}

.button {
  border-radius: 5px;
  padding: 10px;
  background: $primary-color;
  color: white;
}

.button.is-with-icon {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.button.is-with-icon .fa {
  border-right: 1px solid white;
  padding: 3px;
}

.paragraph__cta centers the button horizontally and gives it some breathing room vertically as well, positioning it within the container.
Meanwhile, all styles pertaining to the structure and composition of the button were declared in the .button block. This is how I handle composition with Sassy BEM.

Scoped Styles For React Components

Scoping. The methods by which we prevent style rules from being applied to elements for which they weren't intended. React has no built-in way to do this, so I rely on sass nesting.

The root element of the component gets an id (or classname or attribute). This serves as a namespace for all of the component's styles. This is achieved by nesting all style declarations for the component inside a block with that id. This has the effect that everything compiles down to a descendant selector and is therefore only applied to elements within the given component.

export default function MyParagraph(props) {
  return (
    <p class="paragraph is-warning" id="my-paragraph-component">
      Some warning text. Followed by advice on how to handle the issue. 
      There is a cta button to help carry out this advice in one click.

      <button class="paragraph__cta button is-with-icon">
        <i class="fa fa-wrench"></i>
        Fix automatically
      </button>
    </p>
  );
}
/****** SCSS */
#my-paragraph-component {
  $rootBlock: ".paragraph";

  text-align: justify;

  &.is-warning {
    font-weight: 500;
  }

  #{$rootBlock}__cta {
    margin: 25px auto;
  }

  .button {
    border-radius: 5px;
    padding: 10px;
    background: $primary-color;
    color: white;

    &.is-with-icon {
      display: flex;
      justify-content: space-between;
      align-items: center;

      .fa {
        border-right: 1px solid white;
        padding: 3px;
      }
    }
  }
}

/****** Compiled CSS */
#my-paragraph-component {
  text-align: justify;
}

#my-paragraph-component.is-warning {
  font-weight: 500;
}

#my-paragraph-component .paragraph__cta {
  margin: 25px auto;
}

#my-paragraph-component .button {
  border-radius: 5px;
  padding: 10px;
  background: $primary-color;
  color: white;
}

#my-paragraph-component .button.is-with-icon {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

#my-paragraph-component .button.is-with-icon .fa {
  border-right: 1px solid white;
  padding: 3px;
}

That is all

This is the approach I have been taking to styling for some time now. And it works quite well. If you've been doing something similar or have comments about my methods, do share them in the comments. Cheers!

Discussion (0)

pic
Editor guide