bem-sass

a Sass library for organizing CSS objects

Usage no npm install needed!

<script type="module">
  import bemSass from 'https://cdn.skypack.dev/bem-sass';
</script>

README

bem-sass

Bower version npm version Build Status

bem-sass is a Sass library for organizing CSS objects. It helps you apply BEM architecture to CSS you write with no compatibility issues with both RubySass(>= 3.4) and LibSass(>=3.3).

bem-sass is heavily inspired by Immutable CSS and ITCSS as well as the original BEM methodology. It is a pure Sass implementation of those concepts.

Quick Start

  • Install with Bower: bower install bem-sass --save-dev
  • Install with npm: npm install bem-sass --save-dev

Basic Usages

Once you import bem-sass to your project, you can simply build your own css object like below:


// Menu block

@include block(menu) {
  /*...the menu block styles are here...*/

  @include element(item) {
    /*...the menu item styles are here...*/
  }
  
  @include modifier(horizontal) {
    /*...the horizontal menu styles are here...*/
  }
}

When compiled:

.menu {
  /*...the menu block styles are here...*/
}

.menu__item {
  /*...the menu item styles are here...*/
}

.menu_horizontal {
  /*...the horizontal menu styles are here...*/
}

Configurations

You can configure bem-sass options by configure-bem-sass mixin. Using this mixin is optional. If there have been no custom configurations, the default options are exactly the same as below:

@include configure-bem-sass ((
  default-prefix: "",
  block-levels: (),
  element-sep: "__",
  modifier-sep: "_"
));

default-prefix

Set the default prefix for block mixin.

@include configure-bem-sass((
  default-prefix: "b-" // Set default block prefix to "b-"
));

/* Menu block */
@include block(menu) {
  /*...styles are here...*/
  
  @include element(item) {
    /*...styles are here...*/
  }
}

When compiled:

/* Menu block */
.b-menu {
  /*...styles here...*/
}

.b-menu__item {
  /*...styles here...*/
}

block-levels

Sometimes you may need to define several block types to organize your css object structure especially when you are considering a methodology like ITCSS. You can define your own several block levels by adding level-name(string): prefix(string) to block-levels map.

@include configure-bem-sass((
  block-levels: (
    object:    "o-",  
    component: "c-"
  )
));

/* Media object */
@include block(media, "object") {
  /*...styles are here...*/
}

/* Menu component */
@include block(menu, "component") {
  /*...styles are here...*/
}

When compiled:

/* Media object */
.o-media {
  /*...styles are here...*/
}

/* Menu component */
.c-menu {
  /*...styles are here...*/
}

element-sep, modifier-sep

You can set your own separators for element and modifier respectively.

@include configure-bem-sass((
  // Set separators like Medium.com
  element-sep: "-",
  modifier-sep: "--"
));

/* Promo block */
@include block(promo) {
  /*...styles are here...*/
  
  @include element(title) {
    /*...styles are here...*/
  }
  
  @include modifier(hero) {
    /*...styles are here...*/
  }
}

When compiled:

/* Promo block */
.promo {
  /*...styles are here...*/
}
.promo-title {
  /*...styles are here...*/
}
.promo--hero {
  /*...styles are here...*/
}

Extended Details

Boolean Modifier & Key-Value Modifier

bem-sass supports key-value modifiers. When using modifier, passing a single argument generates a boolean modifier, whereas passing 2 arguments generates a key-value modifier.

// @see https://en.bem.info/method/naming-convention/#block-modifier

@include block(menu) {
  
  /* Boolean modifier */
  @include modifier(hidden) {
    /*...the hidden menu styles are here...*/
  }
  
  /* key-value modifiers */
  @include modifier(theme, morning-forest) {
    /*...the morning-forest themed menu styles are here...*/
  }
  
  @include modifier(theme, stormy-sky) {
    /*...the stormy-sky themed menu styles are here...*/
  }
}
/* Boolean modifier */
.menu_hidden {
  /*...the hidden menu styles are here...*/
}

/* key-value modifiers */
.menu_theme_morning-forest {
  /*...the morning-forest themed menu styles are here...*/
}

.menu_theme_stormy-sky {
  /*...the stormy-sky themed menu styles are here...*/
}

Element Modifier

Elements could also get modified by their own modifiers.

// @see https://en.bem.info/method/naming-convention/#element-modifier

@include block(menu) {
  @include element(item) {
    /* Boolean modifier */
    @include modifier(visible) {
      /*...the visible menu item styles are here...*/
    }
    
    /* key-value modifier */
    @include modifier(type, radio) {
      /*...the radio type menu item styles are here...*/
    }
  }
}
/* Boolean modifier */
.menu__item_visible {
  /*...the visible menu item styles are here...*/
}

/* key-value modifier */
.menu__item_type_radio {
  /*...the radio type menu item styles are here...*/
}

Using Cascades in BEM

// @see https://en.bem.info/method/solved-problems/#using-cascades-in-bem

/* Nav block */
@include block(nav) {
  /*...the default nav styles are here...*/
  
  @include element(item) {
    /*...the default nav item styles are here...*/
  }
  
  @include modifier(theme, islands) {
    /*...the islands themed nav styles are here...*/
   
    @include element(item) {
      /*...the islands themed nav item styles are here...*/
    }
  }
}

When compiled:

/* Nav block */
.nav {
  /*...the default nav styles are here...*/
}

.nav__item {
  /*...the default nav item styles are here...*/
}

.nav_theme_islands {
  /*...the islands themed nav styles are here...*/
}

.nav_theme_islands > .b-nav__item {
  /*...the islands themed nav item styles are here...*/
}

Adjacent Sibling Elements in a Modifier

Given that you want to add a top line to each item of a modified nav block except the first item. With & provided by the original Sass, you cannot achieve this requirement. In that kind of circumstance, you can use adjacent-siblings mixin.

@include block(nav) {

  @include modifier(secondary) {

    @include element(item) {

      // Using & + & produces `.nav_secondary .nav__item + .nav_secondary .nav__item` which we do not expect.
      // Use `adjacent-siblings` here.

      @include adjacent-siblings {
        border-top: 1px solid rgb(0, 0, 0);
      }
    }
  }
}

When compiled:

.nav_secondary > .nav__item + .nav__item {
  border-top: 1px solid rgb(0, 0, 0);
}

Shared CSS Rules Between Elements

Given that nav__item and nav__link have common CSS rules. Since bem-sass enforces immutability on every BEM entity, it seems that the only way to avoid an inevitable code duplication is using Sass placeholder and @extend.

@include block(nav) { 

  %shared-rules { // <--- BAD: A placeholder inside block generates unwanted nested selectors
    display: inline-block;
    height: 100%;
  }

  @include element(item) {
    @extend %shared-rules; 
  }

  @include element(link) {
    @extend %shared-rules;
  }
}

But when compiled, this produces unexpected nested selectors like below:

.nav .nav__item,
.nav .nav__link {
  display: inline-block;
  height: 100%;
}

To avoid this, bem-sass provides def-shared-rules and shared-rules.

@include block(nav) {

  @include def-shared-rules("items") {
    display: inline-block;
    height: 100%;
  }

  @include element(item) {
    @include shared-rules("items");
  }

  @include element(link) {
    @include shared-rules("items");
  }
}
.nav__item,
.nav__link {
  display: inline-block;
  height: 100%;
}

Note that def-shared-rules and shared-rules should be inside of a block.

Caveats

Element and Modifier Cannot be used Stand-Alone

An element(or a modifier) is a part of a block. It has no standalone meaning without it's parent block.

// @see https://en.bem.info/method/key-concepts/#element
@include element(item) { // <-- BAD: element without it's block
  /*...CSS declarations here...*/
}

// @see https://en.bem.info/faq/#how-do-i-make-global-modifiers-for-blocks
@include modifier(theme, islands) { // <- BAD: modifier without it's block
  /*...CSS declarations here...*/
}

When compiled:

Error: element should be inside of a block
Error: modifier should be inside of a block

Avoid Elements Within Elements

The existence of elements of elements is an antipattern because it hinders the ability to change the internal structure of the block. bem-sass prevents you from creating those kind of invalid elements.

// @see https://en.bem.info/faq/#why-does-bem-not-recommend-using-elements-within-elements-block__elem1__elem2
@include block(nav) {
  @include element(item) {
    @include element(link) { // <--- BAD: Attempt to make an element within another element
    }
  }
}
Error: element should not be within another element

Keep BEM Entities Immutable

bem-sass ensures that every BEM entity you create is immutable. It prevents you from reassigning css classes which in turn produces side effects.

// Nav block
@include block(nav) {
  /*...CSS declarations here...*/ 

  @include element(item) {
    /*...CSS declarations here...*/ 
  }

  @include element(item) { //   <--- BAD: Attempt to reassign the nav item styles
    /*...CSS declarations here...*/ 
  }
}

@include block(nav) { // <--- BAD: Attempt to reassign the nav block styles
  /*...CSS declarations here...*/ 
}
Error: in `element': Attempt to reassign `.nav__item` 
Error: in `block': Attempt to reassign `.nav`

The Order of Block Levels Matters

Note that the order of your block levels is important. For example, the following will cause an error:

/* Menu component */  
@include block(menu, "component") { //  <--- BAD: `component` is ahead of `object`
  /*...styles are here...*/
}

/* Media object */
@include block(media, "object") {
  /*...styles are here...*/
}
Error: the `object` level block `media` should not be behind of any `components` level blocks. Your block levels are: object, component

See Also