Writing better CSS – Part 3: bem-plus

Even in the first post in this series, I have noted that styling problems are very different from selector problems. I think the separation is greater than most people realize. In fact, the styles and the selectors can be understood as two completely separate systems, and separating those two systems is a core part of bem-plus.

bem-plus

So the first step is to take the styles of each element of a block, and put them into a mixin. The name of that mixin should always be [block name]-[element name], or in the case of the block itself [block name]-root. We’ll also wrap the now bare selectors in another mixin, which we will include at the base level at the very bottom of the file. You might think that would cause chaos and confusion, but the opposite is the case. We just vastly simplified our structure, and each element now has a unique, easy to search for name.

But let’s take a look at an example. We’ll continue with the example from the previous post. From this:

// main-nav.scss
.main-nav {
	// ...
	
	&__burger {
		// ...
	}
	
	&__list {
		// ...
	}
	
	&__item {
		// ...
	}
	
	&__link {
		// ...
		
		&:hover {
			// underlined
		}
	  
		&--active {
			// different color
	  }
	}
	
	&--active {
		// ...
		
		.logo__image {
			// hidden
		}
		
		.main-nav__burger {
			// different icon
		}
	}
}

// logo.scss
.logo {
	// ...
	
	&__image {
		// ...
	}
}

We create this:

main-nav.scss:

@mixin main-nav-root {
	// ...
	
	&--active {
		// ...
		
		.logo__image {
			// hidden (just as an example)
		}
		
		.main-nav__burger {
			// ...
		}
	}
}

@mixin main-nav-burger {
	// ...
}

@mixin main-nav-list {
	// ...
}

@mixin main-nav-item {
	// ...
}

@mixin main-nav-link {
	// ...
	
	&:hover {
		// ...
	}
	
	&--active {
		// ...
	}
}

@mixin main-nav {
	.main-nav {
		@include main-nav-root;
		
		&__burger {
			@include main-nav-burger;
		}
		
		&__list {
			@include main-nav-list;
		}
		
		&__item {
			@include main-nav-item;
		}
		
		&__link {
			@include main-nav-link;
		}
	}
}

@include main-nav;

logo.scss:

@mixin logo-root {
	// ...
}

@mixin logo-image {
	// ...
}

@mixin logo {
	.logo {
		@include logo-root;
		
		&__image {
			@include logo-image;
		}
	}
}

@include logo;

Note that we have moved the modifiers together with the styles. We only extracted the content of the elements into mixins, not the modifiers and also not &:hover, or media queries or pseudo-elements for that matter.

The mixins we just created are called element mixins. While they are regular mixins, some rules apply to them that do not apply to other mixins, which I will call functional mixins:

  • Do not reuse. Every element mixin should only be used once, in the index below. If you have code inside an element mixin you would like to reuse, abstract that code into a functional mixin and @include that mixin in the element mixin.

This, I think, already helps a lot. If we are looking for a specific element, we can search the entire project for that element (replacing __ with -), and we will always find that element and only that element. Additionally, at the bottom of every scss file (which should only ever contain one block), we have essentially created an index of what that file contains, which further improves orientation.

We wrap that index in it’s own mixin three reasons:

  • Like with single elements, we can now disable the styles of the entire block by commenting out a single line of code (the last one). We have a single “hook” to control the entire block.
  • As all the other code now lives in mixins, it would be odd to not also put this in a mixin.
  • It is easier for a script to detect. We’ll talk about why this is important later.

Keep the index flat and clean! Its only job is to reference the element mixins. Styles, media queries, modifiers and functional mixins are not allowed here.

But let’s take a closer look at the main-nav-root mixin. The &--active modifier is fine. It belongs there. But the other two selectors, .logo__image and .main-nav__burger do not really belong there. The styles for the burger element would really belong to the main-nav-burger mixin and the styles for the logo image do not even really belong into this file.

So let’s tackle that next. Luckily, there’s a tool provided by SCSS made exactly for this case:

@at-root

If you want to understand @at-root you first have to understand &. You have likely used & many times, but it’s worth taking a closer look. & means “Take the selector as we built it up until now, and append more to it.” & is a shorthand. It stands in for the selector, for the “context” we have built up, up to this point.

@at-root works the other way around. Instead of appending, we prepend. Here’s an example:

.foo {
	&__bar { // note that the & essentially stands for ".foo" here
		// once compiled, this results in ".foo__bar {}"
	}
}

.foo {
	.bar { // With nesting, the & is implicit. On this line, you could also write &.bar {
		// once compiled, this results in ".foo .bar {}"
	}
}

.foo {
	@at-root .bar & {
		// once compiled, this results in ".bar .foo {}"
	}
}

Another way to think about this is like a cursor. By using @at-root we essentially move the cursor outside of all the selectors we are currently located in. We move it to the root, so to speak, hence the name.

But note the & we use together with the @at-root, this is the best part. The & doesn’t care about @at-root, it behaves as if it was written inside the .foo context, so it returns .foo, hence the final selector .bar .foo.

Note however that we have to write & at the end, otherwise we end up with just .bar.

Here’s another example:

.card {
	background-color: white;
	
	@at-root .body--dark & {
		background-color: black;
	}
}

This is amazing, because it means we no longer have to define that the card shall have a black background inside the .body--dark selector. We can flip it around. This, in turn, means we can finally define all styles where they belong. This leads to a system where, if you are looking at an element mixin, you can be certain that all CSS code that affects this element in any way is defined here. You do not have to look anywhere else. This is everything. If there’s a bug with this element, this is where you have to look, and nowhere else.

Let’s return to our initial example. We can now do this:

@mixin main-nav-root {
	// ...
	
	&--active {
		// ...
		
		// *poof*
		
		// *poof*
	}
}

@mixin main-nav-burger {
	// ...
	
	@at-root .main-nav--active & {
		// ...
	}
}

@mixin main-nav-list {
	// ...
}

@mixin main-nav-item {
	// ...
}

@mixin main-nav-link {
	// ...
	
	&:hover {
		// ...
	}
	
	&--active {
		// ...
	}
}

@mixin main-nav {
	.main-nav {
		@include main-nav-root;
		
		&__burger {
			@include main-nav-burger;
		}
		
		&__list {
			@include main-nav-list;
		}
		
		&__item {
			@include main-nav-item;
		}
		
		&__link {
			@include main-nav-link;
		}
	}
}

@include main-nav;

logo.scss:

@mixin logo-root {
	// ...
}

@mixin logo-image {
	// ...
	
	@at-root .main-nav--active & {
		// ...
	}
}

@mixin logo {
	.logo {
		@include logo-root;
		
		&__image {
			@include logo-image;
		}
	}
}

@include logo;

That’s it. It’s not that complicated, right? Once you get used to the system, it starts to feel like water, like the air you breathe. It becomes so normal you start to notice its absence much more than its presence.

Rules and recommendations

I think I've given you an overview of the basic idea of bem-plus here. If you want to dive deeper, you can find a full list of rules with explanations for the bem-plus methodology in the bem-plus reference.

To name some of the most important:

  • The index shouldn't contain any other elements than the block and its elements. No modifiers, no other classes. All selectors in the index should have a specificity of 10.
  • In block files, there should only be the index and the element mixins, no other mixins, no loose selectors. Utility code like a reset, font declarations and such should be kept separately, for example in an util directory.

But instead of rehashing what you can find in the reference linked above, here I want to write about the tooling around it, which will allow you to write CSS even faster.

Speeding up writing bem-plus code with live templates

“There’s a lot more code I have to write now”, you might say, and you’d be correct with that assessment. This system is a bit more verbose than what we started with. But first of all, look at all the problems we solved with this system. Isn’t not having to deal with any of those problems ever again worth a couple extra lines of code?

Secondly, look at what we’ve done so far. We have taken a bunch of code that does not follow any sort of pattern, and we have introduced a standardized, repetitive pattern that is always the same. The existence of such a pattern allows us to now automate certain parts of it.

Most modern IDEs allow you to define live templates. We’ll define two of those, which will allow you to write the structure of a bem-plus file much faster.

Installation

IntelliJ IDEs

  1. Navigate to Settings > Editor > Live Templates. In some of their IDEs (like Rider) you might have to open “Other Languages”.
  2. Click on the little Plus at the top of the list and select “Template Group…”. If you have created live templates before, you can also put the live templates we are about to create somewhere else, this is purely for organizational purposes. Name the group however you like. I tend to put all my live templates in the same place, no matter the language, so I simply call this group “custom”.
  3. Click the little plus again, then click “Live Template”
  4. As the abbreviation, enter @tree, or whatever you prefer.
  5. As the template text, enter the following:
@mixin $BLOCK$-root {
    $END$
}

@mixin $BLOCK$ {
    .$BLOCK$ {
        @include $BLOCK$-root;
    }
}

@include $BLOCK$;
  1. Click the Change button at the very bottom and select CSS.
  2. Click the “Edit Variables…” button. An entry with the name BLOCK should already exist. as the Expression, enter this: regularExpression(fileNameWithoutExtension(), "_", "")
  3. Repeat steps 3-7 with the following information:
  • Abbreviation: &__
  • Template text:
&__$ELEMENT$ {
    @include $BLOCKNAME$-$ELEMENT$;
}
  • Variables
    • ELEMENT: (leave empty)
    • BLOCKNAME: regularExpression(fileNameWithoutExtension(), "_", "")
  1. Save the settings.

VSCode

  1. Select Code > Settings… > Configure User Snippets
  2. Search for and open the “scss” snippet file
  3. Add the following two snippets:
"bem-plus tree": {
		"prefix": "@tree",
		"body": "@mixin ${TM_FILENAME_BASE}-root {\\n\\t$0\\n}\\n\\n@mixin ${TM_FILENAME_BASE} {\\n\\t.${TM_FILENAME_BASE} {\\n\\t\\t@include ${TM_FILENAME_BASE}-root;\\n\\t}\\n}\\n\\n@include ${TM_FILENAME_BASE};"
	},
	"bem-plus element": {
		"prefix": "&__",
		"body": "&__${1} {\\n\\t@include ${TM_FILENAME_BASE}-${1};\\n}"
	}

Usage

For those two live templates to work, the name of the file must be the same as the name of the block.

Now, if you create a new scss file for a new block, you can simply type in @tree and press tab. The basic structure with an index and the root element mixin will be created.

The second live template is meant to be used within the index: Type &__ and press tab, now write the name of the element you wish to create. Unfortunately this does not also create the element mixin itself, you still have to do that yourself, but this already speeds up the creation of new elements a lot I think.

How does it compare to BEM?

Let’s have a final look at the list of problems with CSS we defined at the very beginning, to see if we could improve our score. With BEM alone, we got to a score of 25/50.

Specificity

We’ve already solved this with BEM. I don’t think this needs further improvement. 10/10

Findability, Scattered Styles

If you are looking for a certain element, you can always search for it. By doing so, you will always only find one result, the one you expect. In that place you have found, you can be certain to find what you are looking for. There is no other place where it could be. Everything about the element you investigate is defined here.

I don’t think that this can be improved further, thus, both of those sections get a 10/10.

Collaboration

With BEM and now bem-plus we have introduced a standardized way of doing things. While with BEM there is still a lot of freedom on how to structure your code, with bem-plus there is usually only one correct way to do things.

bem-plus is also very visible. It is clearly very different from standard CSS code. If you open a bem-plus file, you can see at first glance that there’s a system here. If someone disregards this system and writes regular CSS code somewhere, this sticks out and is thus easy to detect.

There is no way yet to programmatically enforce the bem-plus syntax, so this is not perfect yet.

I think BEM has improved the situation for the most part, but I also think it got improved again by bem-plus, so I’d give bem-plus as it is today an 8/10.

Missing Standardization

Unlike BEM, with bem-plus we now have a highly standardized structure. It is so standardized in fact, that we can now use those scss files to derive useful information: What blocks exist in this application? What elements do belong to this block?

This information is super easy to obtain: Just have a look at the index! That information is not only useful for us as developers, it is also useful to generate stuff. And that’s exactly what the final part of our long journy will be about.

The fact that we can now even consider such a thing means that our code is now highly standardized. As such, bem-plus gets a score of 10/10 from me for standardization.

This leads to a final score of 48/50. I might be biased.

I’m still thinking about those final two points, though. I think there’s a way, but I have not fleshed it out yet. For now, let’s have a look at how we can profit from bem-plus beyond CSS.

Packages

One of the main reasons for using bem-plus is that it structures CSS code very well. Things are a lot easier to find because they are sorted, named, organized, where they belong. A benefit of this standardized ordering of things, in turn, is that a computer can also be taught to understand this structure, and based on the gathered information, new code can be generated.

I have built one package so far and am considering some other ones too, here's what they do:

@bem-plus/class-generator

The class generator takes bem-plus code and generates TypeScript classes. You can then write your own JS classes extending the generated ones, gaining various benefits. For example, you no longer have to write querySelectors, and dealing with modifiers is a lot simpler. You also gain autocomplete, so you always know which elements and modifiers are available. Instead of writing:

class MainNav {
	constructor() {
		const links = document.querySelectorAll('.main-nav__link');

		links.forEach((link) => {
			link.addEventListener('click', () => {
				link.classList.toggle(
					'main-nav__link--active',
					link.classList.contains('main-nav__link--active')
				)
			})
		})
	}
}

You can now write this:

import { MainNavBase } from '@/.bem-plus'

class MainNav extends MainNavBase {
	constructor() {
		this.links.forEach((link) => {
			link.el.addEventListener('click', () => {
				link.active = !link.active;
			})
		})
	}
}

You can find the @bem-plus/class-generator package and its documentation on npm.

@bem-plus/sass-generator

If you look at a bunch of HTML which uses BEM, you can quite easily figure out which blocks, elements and modifiers exist in this project. More than that, a computer could also figure this out. So wouldn't it be amazing if you didn't have to write all of the block files with their many element mixins, because they were automatically generated? This would speed up development with bem-plus even further and it would also allow a much faster migration of a BEM project to bem-plus.

I have built a first draft of this package here, but have faced two main challenges:

  • People don't really use HTML as such, they use JSX or Twig or Razor, and they mix it with JS or PHP or C# code. Building this package would require a separate parser for each flavor of HTML. This isn't impossible, but it sure does make things more complicated.
  • Ideally, the block files would be generated on save, however I found that this interferes especially with IntelliJ IDEs. They simply don't expect code being generated, and take a couple seconds to realize something happened. VSCode is much better at this, but it's also a bit annoying if code gets generated, or worse, removed, while you're working, so the whole concept kind of fell apart at this point.

Because of this, I'm considering rewriting this as a CLI tool, but I yet have to find the time for it.

@bem-plus/autoloader

The data available with bem-plus can be used to assist with chunk loading, which ensures that only the CSS and JS gets loaded by the browser that is actually needed, improving performance. This is an established practice in modern web development, but it can be tricky to set up if not working with a framework like React.

I have previously built something like this into a project using the @bem-plus/class-generator package, but I can imagine this being a separate package, fully automating the process.

@bem-plus/linter

As the name implies, this would be a stylelint plugin that ensures and automatically fixes violations of the rules defined in the reference.

Conclusion

Since I have come up with this system around 2020, I have used it in every project I can. Since its introduction, scoping, specificity and scaling issues have been reduced to virtually zero. That doesn't mean my CSS code is 100% free of bugs every time no exceptions. Some browsers still behave weird, things still get forgotten sometimes when writing CSS based on a design, And if the design changes, changes still get made that do not fully account for the context the changes take place in.

But damn.

As of today, I've worked at three different companies since the inception of bem-plus, two of them were open to the introduction of bem-plus. I do not have exact statistics, but I'd argue that both times, we were able to reduce time spent debugging CSS by half, at least.

So take that as a glowing, though obviously biased recommendation.