Maintanable CSS architecture (how I stopped worrying and learned to love the BEM)

CSS is hard! It is hard because CSS is easy. It reads like a paradox but bear with me. CSS is relatively easy to learn, at least compared to learning programmable languages. Majority of the styling rules are clear and easy to grasp. From my experience, teaching someone from zero to reasonably-understands-CSS-and-can-style-a-web-page takes roughly a couple of weeks. I ran a class at work and nearly everyone could style elements after verbal instructions. This is rare in regular programming. Explaining what an object instance is from a class takes at least a week.

CSS is hard when you start dealing with organising layouts. Knowing what rules and where to apply is almost an art in itself. At a certain point, I cannot even explain to someone why I chose to avoid using margins and used paddings instead. But it is the subtle decisions that play the bigger part in determining whether your CSS architecture succeeds or fails. Success in this case, means deleting one preprocessor file won’t affect an unrelated section in your project. Every section should work independent of each other.

I wrote this post because I was surprised at my job how little time new CSS takes to write despite the fact that it was setup nearly 3 years ago. The only styleguide is a pre-commit hook that warns about nesting. It rarely flags except when dealing with something like

@media(min-width: 920px){
  .block__element--modifier::after {}
}

This is an edge case and not breaking any rules outlined below but a reminder that the pre-commit hook still works.

Please note that this post assumes you are familiar enough with CSS properties, preprocessors, and specificity. In other words, if you are at an intermediate understanding of CSS but you struggle to write maintainable CSS, then maybe it will help. If you find yourself constantly having to specify selectors like .modal > div.form-wrapper, then by all means continue reading this. If you keep overwriting bootstrap styles, maybe this is also the post for you (I know this because I used to have lots of nested .btn in my CSS).

There are several rules that are easy to follow and yet magically keep you out of trouble:

Always use classes

This one is rather straight-forward. Try as much as possible to avoid referring actual tag names or #ids. Every time you break it, drop a coin in a jar. If you find yourself actually looking for a coin jar, you have already failed. The only time it is acceptable to break this, is when you have a third-party element that is not following this rule. Even then, send a pull-request to the project explaining why they should learn to write maintainable CSS.

Learn, understand and embrace BEM

BEM is a rather simple concept to understand. It stands for Block-Element-Modifier. It sounds scary at first but it is a relatively simple idea. When writing CSS, try to break down portions of your page into independent components/sections (I will use block and component interchangeably). Lets take a landing page. You have a navigation bar, a sidebar, a call to action, an annoying popup modal, and a contact section/footer. And just like that you now understand what Block stands for in the acronym. Think of a block as part of your site that will always look and behave the same way no matter where it is found. In some circles, they refer to this as a component. Do not think of this in terms of functionality. Two chatboxes are not the same block if you have one that is small and stickied to the bottom of each page vs one that covers an entire page and functions as it’s own page. As far as functionality is concerned they might do the exact same thing, but how they look or behave relative to the rest of the page is how they should be differentiated. Each block should be given its own class. The stickied chatbox, should be .small-chatbox compared to .full-page-chatbox in the other case.

Each block is composed of child elements that together form the entire block. A barebone chatbox comprises of a chat message(s), text input box and a submit button. Each one of these is an element. Try to break down each block to its granular elements. If you have lots of previous chat messages in the previous example, dont think of the list of chat messages as one element but rather many elements. It is perfectly acceptable to wrap elements inside other elements. Just keep in mind that any CSS you write for the element should ONLY address the element styling. As a naming convention, elements of a block are given .block__element class. Below is how you would write our chat messages.


< div class='chatbox' >
  < div class="chatbox__messages-wrapper">
    < p class="chatbox__message">< /p>
    < p class="chatbox__message">< /p>
  < /div>
  < !-- TODO: form for the chatbox -->
< /div>

Modifiers are probably the most subjective part of BEM. Modifiers are CSS class names given to an existing block or element class but with slightly altered behaviour. A modifier isn’t meant to exist on its own but to enhance another block/element. In our chatbox example, lets assume a user can favourite a message. If starred message looks exactly like any other message with the exception of a golden border and slightly larger text – then by all means just add a modifier class to it. In the same scenario, if a chatbox is blinks after receiving a new message, then add a modifier class for the block. Modifier classes are written as .block__element--modifier for modifier applied to an element and .block--modifier when applied to a block. Keep in mind, a modifier CSS class does not exist on its own. If a block or element has a modifier class, it should also have a block or an element class too (>= 2 CSS classes). If you find yourself writing class="chatbox__message--starred", then you are not following BEM. It should be class="chatbox__message chatbox__message--starred". It looks odd at first but you will understand later why this is very powerful. Like the name suggests, they just modify an existing block/element. If you are unsure whether you should add a modifier class compared to creating a new element, then play it safe and create a new element. You can never go wrong with creating a new element. My rule of thumb is a modifier should not have more than 3 lines of CSS.

The hardest part of BEM is breaking down the actual layout into independent blocks. It comes with practice and, if lucky, a good UI layout.

Not everything is a block (a word about objects)

One of the pitfalls of learning BEM is trying to treat everything in your CSS as a block. Lets talk about layouts. Most real-world sites have gutters and grids fencing components from others. Most CSS helper libraries have grid classes. Lets call these layout helper classes. They exist to organise other blocks. If your page consists of only these layout helper classes, then you should see a blank page. Using a theatre analogy, they are the stage directors compared to blocks that act as the main show. You don’t need to follow BEM to write them. If you do mistake them for blocks in BEM, nothing will go wrong. But it helps to know that they exist and helps to avoid going full BEM.

Modules

Modules are large blocks that exist only once on your site. Regular blocks can appear on several pages while a module can appear on one page only. Lets say your shopping site has a section where a user can edit their account details. That is an account editing module. However, an account summary block can appear both on the edit account page and checkout pages. As a result, most CSS files will have a few modules but a lot of components. Modules still follow BEM’s guide in writing classes but they are useful to separate from regular components to ensure they take priority.

Miscellaneous

Like any large CSS project using a preprocessor, you end up requiring several helper files with things like variables, breakpoints, mixins, third party libraries, prefixes (for the unfortunate), helper functions, resets etc. Often, you drop these into your project before writing a single line of CSS.

Bringing it all to life (using SASS)

I will use SCSS but you can replace it with stylus, less etc and nothing should change. When using a preprocessor, you should always have a main file that you use as your entry point. I aptly named it main.scss. Below is a guideline on how you import the rest of the files inside this main file. Note that CSS specificity requires us to import first the less specific files, with the most important styles being the module files.

// main.scss
  - @import 'miscellaneous/main'    // Importing all overhead
  - @import 'helpers/main'          // The layout files folder
  - @import 'components/main'       // All blocks
  - @import 'modules/main'          // Modules

Inside each of these folders, we are importing the entry file which links us to all files. components/main.scss will contain

  - @import 'navigation-bar'        // contains styles for the navigation component only
  - @import 'chatbox'               // chatbox block styles

Assuming the same markup from previous chatbox example, the chatbox SCSS file (i.e components/chatbox.scss
) will look like


.chatbox {
  box-shadow: 2px 2px $fade-shadow; // color variable for my shadows

  &--alert {
    border: 1px solid $annoying-blink-color;
  }

  &__message-wrapper {
    padding: 5px;
  }

  &__message {
    font-size: 16px;

    &--starred {
      text-transform: underline;
      font-size: 18px;
      background-color: $golden-background;
    }
  }
}

This will output:


.chatbox {
  box-shadow: 2px 2px #000;
}
.chatbox--alert {
  border: 1px solid red;
}
.chatbox__message-wrapper {
  padding: 5px;
}
.chatbox__message {
  font-size: 16px;
}
.chatbox__message--starred {
  text-transform: underline;
  font-size: 18px;
  background-color: yellow;
}

First thing you should notice here, block modifiers always come before element styles. This is good way to ensure any element property is not overwritten by a block’s styling. CSS specificity should automatically protect us from such a scenario but you can never be too careful.

The other but possibly the most important things about BEM, there is NO NESTING. All my css consists of single classes only. My markup can contain up to two BEM classes but specificity is driven by the last class to be declared. All my modifiers will take priority over the block and elements. This is the desired behaviour. Modifiers are meant to overrule the original styles. The lack of nesting guarantees my CSS wont be affected if I decide to change one of the classes. It also gives that desirable specificity graph. I mentioned above how my company’s pre-commit hook will occasionally throw a false flag for @media(min-width: 920px){ .block__element--modifier::after {} }. This is still valid because we are only styling one class without referencing other selectors.

On the subject of changes breaking unrelated sections of your site, deleting one component only requires you to delete the import line and then the component style file. It wont break any other section. Deleting a module acts in a similar manner. As long as your specificity relies on the file position, everything will magically keep working.

And that is pretty much it. Follow these rules and your CSS never be easier to write. In the future, I will explore why modules are important and why we import them last. It will require more real world examples with various permutations where regular components dont suffice on their own.

Further readings to dive deep into why this style works so well:
Specificity Graph
Nesting your BEM

Advertisements