Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active March 7, 2024 09:14
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save loilo/17f5a3e8bd2743d55e9b4cbf5cae047e to your computer and use it in GitHub Desktop.
Sass mixins for BEM

ATTENTION!

I keep this Gist for archival reasons, however I strongly recommend against using it. As I discovered after several weeks in production usage, these BEM mixins cause unexpected, unfixable and hard-to-debug selectors in some cases (especially when nested in some ways).

Sass mixins for BEM

This is a utility with three simple Sass mixins for writing BEM as DRY as possible, heavily inspired by Hugo Giraudel's article on CSS Tricks.

It exposes three Sass mixins: block, element and modifier.

Block

A block must always be at the top level. It can include any of the three provided mixins.

@include block ('alert') {
  color: black;
}

// becomes

.alert {
  color: black;
}

Element

An element mixin

  • can be placed inside a block

    @include block ('alert') {
      color: black;
    
      @include element ('icon') {
        background-image: url('info.svg');
      }
    }
    
    // becomes
    
    .alert {
      color: black;
    }
    .alert__icon {
      background-image: url('info.svg');
    }
  • can be placed inside a block modifier

    @include block ('alert') {
      @include modifier ('warn') {
        @include element ('icon') {
          background-image: url('warn.svg');
        }
      }
    }
    
    // becomes
    
    .alert--warn__icon {
      background-image: url('warn.svg');
    }
  • can take multiple parameters to represent multiple elements

    @include block ('alert') {
      @include element (icon, message) {
        padding: 1em;
      }
    }
    
    // becomes
    
    .alert__icon, .alert__message {
      padding: 1em;
    }
  • can (with care!) be placed inside other elements. It's not the BEM way to nest element selectors, however this is allowed for special cases that cannot be expressed through BEM, for example handling pseudo classes:

    @include block ('alert') {
      @include element ('close') {
        &:focus + {
          @include element ('icon') {
            background-image: url('info-closing.svg');
          }
        }
      }
    }
    
    // becomes
    
    .alert__close:focus + .alert__icon {
      background-image: url("info-closing.svg");
    }

Modifier

A modifier mixin

  • can be placed inside a block

    @include block ('alert') {
      color: black;
    
      @include modifier ('inverse') {
        color: white;
        background-color: black;
      }
    }
    
    // becomes
    
    .alert {
      color: black;
    }
    .alert--inverse {
      color: white;
      background-color: black;
    }
  • can be placed inside an element

    @include block ('alert') {
      @include element ('icon') {
        background-image: url('info.svg');
    
        @include modifier ('hidden') {
          display: none;
        }
      }
    }
    
    // becomes
    
    .alert__icon {
      background-image: url('info.svg');
    }
    .alert__icon--hidden {
      display: none;
    }
  • can be placed inside other modifier mixins to represent modifier combinations

    @include block ('alert') {
      color: black;
    
      @include modifier ('inverse') {
        color: white;
        background-color: black;
      }
    
      @include modifier ('warn') {
        background-color: yellow;
        
        @include modifier ('inverse') {
          color: yellow;
          background-color: black;
        }
      }
    }
    
    // becomes
    
    .alert {
      color: black;
    }
    .alert--inverse {
      color: white;
      background-color: black;
    }
    .alert--warn {
      background-color: yellow;
    }
    .alert--warn.alert--inverse {
      color: yellow;
      background-color: black;
    }
  • can take multiple parameters to represent modifier combinations

    @include block ('alert') {
      color: black;
    
      @include modifier ('inverse') {
        color: white;
        background-color: black;
      }
    
      @include modifier ('warn') {
        background-color: yellow;
      }
    
      @include modifier ('warn', 'inverse') {
        color: yellow;
        background-color: black;
      }
    }
    
    // becomes
    
    .alert {
      color: black;
    }
    .alert--inverse {
      color: white;
      background-color: black;
    }
    .alert--warn {
      background-color: yellow;
    }
    .alert--warn.alert--inverse {
      color: yellow;
      background-color: black;
    }
  • can negate modifiers by prepending a !

    @include block ('alert') {
      color: black;
    
      @include modifier ('error', '!active') {
        color: pink;
      }
    }
    
    // becomes
    
    .alert {
      color: black;
    }
    .alert--error:not(.alert--active) {
      color: pink;
    }
    
@warn "Please consider to stop using the BEM mixins since they can create unexpected incorrect selectors in some cases (especially for deeply nested rules)."
@function bem__negateable-name ($value) {
@if str-slice($value, 1, 1) == '!' {
@return (
negate: true,
name: str-slice($value, 2)
);
} @else {
@return (
negate: false,
name: $value
);
}
}
$bem__context: null !default;
$bem__block: null !default;
$bem__last-modifiable-context: null !default;
$bem__last-block: null !default;
$bem__last-element: null !default;
/// Opens a BEM block
@mixin block ($block) {
$oldContext: $bem__context;
$oldBlock: $bem__block;
$oldModifiableContext: $bem__last-modifiable-context;
$oldLastBlock: $bem__last-block;
$bem__block: $block !global;
$bem__context: block !global;
$bem__last-modifiable-context: block !global;
$bem__last-block: $block !global;
.#{$block} {
@content;
}
$bem__context: $oldContext !global;
$bem__block: $oldBlock !global;
$bem__last-modifiable-context: $oldModifiableContext !global;
$bem__last-block: $oldLastBlock !global;
}
/// Attaches a BEM modifier
@mixin modifier ($modifiers...) {
$oldContext: $bem__context;
// Preserve context inside nested modifiers
@if index((block-modifier, element-modifier), $oldContext) == null {
$bem__context: '#{$oldContext}-modifier' !global;
}
// Create a string of selectors
$selectors: '';
@if $oldContext == 'block' {
$parentSelectors: selector-parse('#{&}');
@for $j from 1 through length($parentSelectors) {
@if $j != 1 {
$selectors: $selectors + ', ';
}
@for $i from 1 through length($modifiers) {
$modifier-data: bem__negateable-name(nth($modifiers, $i));
$negate: map-get($modifier-data, 'negate');
$modifier: map-get($modifier-data, 'name');
@if $i == 1 and $negate {
$selectors: $selectors + '.' + $bem__last-block;
}
$parentSelector: nth($parentSelectors, $j);
$appliedParentSelector: nth($parentSelector, length($parentSelector));
@if $negate {
$selectors: $selectors + ':not(' + $appliedParentSelector + '--' + $modifier + ')';
} @else {
@if $i == 1 {
$selectors: $selectors + nth($parentSelectors, $j) + '--' + $modifier;
} @else {
$selectors: $selectors + $appliedParentSelector + '--' + $modifier;
}
}
}
}
@at-root {
#{$selectors} {
@content
}
}
} @else {
$modifier-list: '';
$item-to-modify: null;
@if $bem__last-modifiable-context == 'block' {
$item-to-modify: $bem__last-block;
} @else if $bem__last-modifiable-context == 'element' {
$item-to-modify: $bem__last-element;
} @else {
@error "The 'modifier' element cannot appear outside a 'block'.";
}
@each $modifier in $modifiers {
$modifier-data: bem__negateable-name($modifier);
$negate: map-get($modifier-data, 'negate');
$modifier: map-get($modifier-data, 'name');
@if $negate {
$modifier-list: $modifier-list + ':not(.#{$item-to-modify}--#{$modifier})';
} @else {
$modifier-list: $modifier-list + '.#{$item-to-modify}--#{$modifier}';
}
}
$selectors: selector-unify('#{&}', $modifier-list);
@at-root {
#{$selectors} {
@content
}
}
}
$bem__context: $oldContext !global;
}
/// Opens a BEM element
@mixin element ($elements...) {
$oldContext: $bem__context;
$oldModifyableContext: $bem__last-modifiable-context !global;
$oldElement: $bem__last-element !global;
$bem__context: 'element' !global;
$selectors: '';
$at-root: false;
// Directly inside a block
@if $oldContext == 'block' {
@for $i from 1 through length($elements) {
@if $i != 1 {
$selectors: $selectors + ', ';
}
$selectors: $selectors + '&__#{nth($elements, $i)}';
}
// Inside another element
} @else if $oldContext == 'element' {
$at-root: true;
@for $i from 1 through length($elements) {
@if $i != 1 {
$selectors: $selectors + ', ';
}
$selectors: $selectors + '& .#{$bem__last-block}__#{nth($elements, $i)}';
}
// Inside a modifier
} @else {
@for $i from 1 through length($elements) {
@if $i != 1 {
$selectors: $selectors + ', ';
}
$selectors: $selectors + '& .#{$bem__block}__#{nth($elements, $i)}';
}
}
@if $at-root {
@at-root {
#{$selectors} {
@content;
}
}
} @else {
#{$selectors} {
@content;
}
}
$bem__context: $oldContext !global;
$bem__last-modifiable-context: $oldModifyableContext !global;
$bem__last-element: $oldElement !global;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment