I have been working in the web for a long time, and I have worked full-time professionally at UL for almost 5 years. In that time, I’ve learned a lot about how developers perceive CSS, and why so many projects are absolutely littered with novice-level CSS.
BEM was introduced a few years ago to deal with one of CSS’ biggest problems: taxonomy, the art of naming things. Its primary goal was the provide a sane, sustainable method for writing class names. Another goal was to create a sustainable approach for changes in state.. For example, BEM provides an answer for “How do we style a blog post (block) that is featured (modifier)?” Consider some of these candidates
.blogPostFeatured {}
.blog-post.featured {}
.blog .post.featured {}
None of these fully address the application of a block-specific featured state, and the intended portability of a CSS class. BEM methodology here is going to be as atomic as possible with components (like a post), and as verbose as possible to avoid collisions. For example, using .featured
with .blog
is fine, but what if another entity, like .page
comes along, and also uses the .featured
class? What happens when these two entities (posts and pages) need different styles for featured?
Tweaking .featured
for one entity will also affect the other. Unintended changes across the app lead to what I call this the Pick Up Sticks problem. For this reason, using verbose Modifiers is the solution to the problem. Consider the following SCSS. Notice that .post--featured
only works with .post
, and so the modifier’s effect is isolated to the block.
.post {
&.post--featured {} // => .post.post--featured
}
.page {
&.page--featured {} // => .page.page--featured
}
Now, examine the accompanying HTML markup…
<!-- Works great! -->
<div class="post post--featured"></div>
<div class="page page--featured"></div>
<!-- Featured states have no effect when misapplied -->
<div class="page post--featured"><div>
<div class="post page--featured"></div>
Essentially, this creates namespaces for the .page
and .post
blocks, and any of the modifiers for these blocks are useless outside of the associated namespaces. This is a powerful, clean, sustainable approach that will help you tame and harness the cascade, and master CSS.
Styling relationships
When parent-child relationships exist within a data model, it is common to see that relationship reflected by markup and styles. Consider the following data model, written in TypeScript.
class Post {
public comments: Comment[];
}
class Comment {}
It’s easy to see that Post
has many Comment
, as indicated by the Post.comments
array of Comment
objects. How do we turn this into a sustainable CSS design system?
First, start with two root-level selectors to represent the Post
and the Comment
.
.post {}
.comment {}
Since Post
has a property to contain many comments, represent this relationship in with an Element (preceded by two underscores) in the SCSS:
.post { // block
.post__comments {} // element
}
.comment {} //block
Have a look at the markup that could be styled by this stylesheet…
<div class="post">
<div class="post__comments">
<div class="comment">…</div>
<div class="comment">…</div>
<div class="comment">…</div>
</div>
</div>
So far so good! It’s clean, easy to understand, easy to maintain, and it’s only the slightest bit verbose. You can see that the Post block contains a Post Comments Element, and we put many Comment blocks into the Post Comments Element.
Let’s continue to style two Comment blocks when they are next to each other in the Post Comments Element… We can use a variant of the lobotomized owl to create a simple spacing system that doesn’t bring unnecessary gaps of whitespace in our page.
.post {
.post__comments {
> .comment + .comment {
margin-top: 1.5rem;
}
}
}
Styling boolean states
Next, consider representing boolean states on a Block. The boolean test might look like this…
class Post {
comments: Comment[] = [];
get hasComments(): boolean {
return this.comments && this.comments.length > 0
}
}
The boolean is represented in SCSS as a Modifier:
.post {
.post--has-comments {} // post.hasComments() === true
.post--no-comments {} // post.hasComments() === false
.post__comments {}
}
When the post has comments, our markup will represent this by including the modifier with the block:
<div class="post post--has-comments">
<div class="post__comments">
<div class="comment">…</div>
<div class="comment">…</div>
<div class="comment">…</div>
</div>
</div>
We are now able to modify the Post appearance, as well as the children, when the has-comments
modifier is added:
.post {
// post.hasComments() === true
&.post--has-comments {
background: url('comments-icon.png') top right;
// post.comments when post.hasComments
.post__comments {
border: solid 1px #ccc;
}
}
// post.hasComments() === false
&.post--no-comments {
.post__comments { display: none }
}
}
The has-comments
state can be used to leverage the cascade (the C in CSS), to further affect descendant elements, without a bunch of bloat.
<div class="post post--has-comments">
<div class="post__comments"></div>
</div>
Our class names are written to represent our schema, its relationships, and its states. This prevents the pick-up sticks problem, and makes the entire project easier to style. Consider a new scenario where a blogging platform allows multiple blogs to exist, so we need to style a series of posts…
class Blog {
title: string;
posts: Post[]
}
No problem! We already have styles for Post
so we just need to author the styles for the Blog block which contains many Post blocks. We’ll add a simple spacing rule, and some nice styles for a title. We also benefit from the styles that were written for Comments
within Post.
.blog {
.blog__title { font-weight: bold; font-size: 2.5rem; }
.blog__posts {
// within .blog__posts, any direct-descendant post that is
// after any post gets a margin-top to space things out
> .post + .post { margin-top: 1.5rem; }
}
}
By being verbose, and using a reliable strategy to map state and schema to CSS appearance, we avoid a host of architectural issues that have been confusing developers for decades.
Putting it together
Here’s a cheat sheet to help apply BEM methodology to the mapping of objects to class names.
When you’re styling… | Use… | For example… |
---|---|---|
A business entity (a row in a database) | a Block | .post .comment .user |
A property on an entity (a column in a database) | an Element | .post__title .post__content |
A child relationship | an Element filled with blocks | .post__comments > .comment + .comment .comment__author > .user |
A state | a Modifier | .post--is-featured .comment--is-post-author .user--is-admin |