Patterns for scaling CSS

How we write CSS has changed a lot in the recent years. Many developers do not write vanilla CSS at all anymore, BEM seems to be the industry standard for structuring the codebase and the advent of SPA’s has magnified it’s shortcomings. But in some ways things have not changed, we are still often going down the rabbit hole of specificity wars and rampant complexity, and finding some new problems on the way too (e.g. nesting). Until CSS, as a language, can accommodate for these requirements, we need to constantly re-evaluate the patterns we use to make sure we minimise those problems and can tackle smartly new ones. This blog post looks at some of my findings when exploring writing good, modern CSS, treating BEM as a base, preprocessors as enhancement and picking up things from variousframeworks on the way.

Using BEM

BEM is well adopted, but also quite often misunderstood. People tend to take BEM solely as a naming convention while the main goal of it is a component architecture. Hopefully, it’s not a surprise that these are not the same. As a naming scheme, BEM is mostly an eyesore, because at first sight, you sacrifice the power of CSS and it’s expressive (all too expressive) selectors for a fiddly name-spacing mechanism.

As a base for component architecture, BEM is quite decent. While designing a system we need to aim for orthogonality for our components and BEM helps us to do that by giving a scheme for encapsulation (alas, poor man’s encapsulation, but still). But consider the below:

.block-one .block-two

This is as a BEM anti-pattern; one component is modifying the other one. If this pattern is rampant in your stylesheet, then no one declaration is definite and without trawling through the styles (or opening your dev tools) you will not know if your rule has been applied. Add to that media-queries and you’ve lost your battle before it has even begun - BEM is giving you no benefit.

BEM is a convention and so it’s really important that your team understands the why (“Why do we use certain selectors and not others?”), what (“What is a block, element and modifier?”) and where (“Where do I put my stuff?”). There should be no ambiguity in the answers to these questions. As with any convention, consider the help of preprocessors and/or linters here, as well writing documentation for your project (although that is only useful if people read it).

One problem with BEM is the scope of block and element, especially if there is no correlation to components in the rest of your codebase (e.g. your javascript is just a bunch of jQuery scripts) or it has a different idea about them (e.g. you are using React without considering it’s relation to CSS). Tying in all of your code into a component architecture helps with defining these boundaries. You can as well get rid of element completely, of which I wrote in one of my previous blog posts.

DRY, smartly

In CSS, DRY (“Don’t Repeat Yourself”), quite often does not mean what it should. It should mean: “Don’t repeat yourself. Find the right abstraction for your duplicated code.” In CSS land, it often means: “Don’t repeat yourself. You just wrote something twice”.

To do DRY effectively, you need to know well the context. For CSS, the context is the design. The more your abstractions exist on the design already (e.g. a variable holding a given font style) the easier it is to translate ongoing changes to the design. Ideally, when talking with designers and the rest of your team about the designs “outside” of the code, you are using those same abstractions. This helps communication, gives your designers more ownership over the end product and helps you in keeping the codebase DRY. When going from design (ideas) to tangible products it’s crucial to think about how changes are going to happen because changes will always happen. Having a common language when speaking about the designs (not only of components but also of lower-level things like font-sizes and colours) is extremely beneficial, not only in terms of DRY.

That said, DRY should be also employed on a much more granular level, within selectors. Having the padding of a button to be relative to the font-size of the text it contains helps to accommodate not only for design changes but also different users (people using zoom, different viewports, different default font size etc.). This applies in many cases and CSS has a lot of mechanisms helping you achieving this (like the modern abundance of relative units).

Don’t go on a class diet

In the early days of BEM, people complained heavily about the amount of classes needed to be added to HTML. This is certainly true; but it also makes it explicit what a given tag is, which is certainly more valuable. The important bit here is the ratio of signal to noise. “Pure” BEM tends to be quite noisy (e.g. you repeat your block name in blocks, elements and modifiers - .block-name, .block-name__long-element-name, .block-name__long-element-name—christmas theme.) so there are many techniques to reduce it, like string concatenation (the & operator in SCSS), leading dashes for modifiers (.-christmas-theme), make BEM wholly encapsulated in mixins (@include block(ad-module)) and other inventive ideas. I’ve not found any hard and fast rules here - I think how inventive you want to get depends mostly on what your team wants to achieve.

It’s important to realise that classes are your best bet when writing robust selectors. It’s easy to assume that you will have one h1 and h2 style, but really that’s rarely the case. Classes convey a lot of meaning while keeping a degree of flexibility. You can be explicit about what a selector is or is not, and you can combine many of them frictionlessly in HTML. Combining classes is, of course, a very common source of bugs, but with the help of affixes we can bring them under control. E.g.:

<div class=“block block—mod”>

The above is just a simple modifier pattern from BEM. It’s incredibly useful, as long as the class is defined as:

.block.block—mod {}

This way we know that .block—mod will never work by itself. This increases specificity, but in a true component architecture, specificity needs to be as flat as possible anyway. You need few, predictable patterns for your selectors (which usually means classes + the odd special selector, like ~ or :before). Your components should always override base styles and their order should never matter.

You can combine multiple modifier classes and when a conflict arises from cascade (i.e. in your encapsulated component), you can just create another modifier class which has the functionality you want while keeping the other modifiers intact (for the most part, cascade should never matter). Watch the size of your modifiers - if they are complex, perhaps your abstraction has outgrown itself and you need a new one. Modifiers should be simple and used for theming, adding context to components (e.g. .block—in-sidebar) and states (e.g. .block—is-open).

Another great class pattern is style classes:

.h-block {

.h-text-align-center {
text-align: center;

Some might shudder in disbelief, but this is actually a great technique to add some very common rules in places where components do not make sense, or you are just experimenting. Helpers are quite similar to modifiers, with the difference that helpers are independent and will always have lower specificity - so we should not use them on components. Two important rules to making helpers are:

  1. Keep them simple - so you can infer exactly what they are doing just from looking at the name. Ideally they have no more than two properties.
  2. Give them a clear affix - so it’s unambiguous what they are (.h—block, .style-block, .display-block-util etc.).

Helpers are a very common pattern, but their role in a framework can differ widely (in the past, some advocated building a whole system around them, as if they were inline styles).

A note on code reuse: it’s best tackled using preprocessors. Extending in vanilla CSS in error-prone: you will start to have to depend on specificity and cascade heavily, which is the opposite of what you want. You want to minimise the problems arising from using these because they tend to grow problems exponentially in big CSS codebases. Again, class selectors, used properly, help with this greatly.

Keep it simple

Did you ever use calc? It lets calculate values in the browser, like this:

.module {
width: calc(100% - 68px);

It’s support in modern browsers is great so you definitely should. Things like calc are immensely valuable because they let you remove all of that indirect cruft that you used to have to do to solve your problem - it adds not only functionality, it adds expressiveness. The lack of it, plus browser quirks, is what made CSS such a frustrating language in places (e.g. layout). You had to accumulate a large body of knowledge to get anything done in a reasonable amount of time.

These days, you can make things simpler and we should abuse that. If position: absolute does the exactly same job for you as display: inline-block, you should think about not the end-effect but as well about the accuracy of what you are trying to express. The best example of this is float. float has a very specific meaning in laying out things, but is used extensively in grids.

CSS is often hard to read for beginners; being a declarative language describing presentation, when there is a strong disconnect with what we see and what we write, we need to remember internally what effect a property (or a given set of properties) will have (or fiddle with it in the dev-tools). This is a waste of time. We should strive (whenever possible) to make things simple and intuitive, not only to beginners, but also to our future selves (this is a well known design concept, but it’s usage in the wild varies).

Don’t be fooled in thinking that these patterns are only for big projects - less complexity, better readability and good modularity helps everywhere. The CSS we need to write became more complex in recent years because of the rise of responsive design and SPA’s, both of which it was never initially designed for. But we are at a point where browser support of key CSS is consistent, and the language itself is evolving at a good pace. You don’t need a wizard’s hat anymore to do it and who knows, we might even finally get over vertically centring things.

At Red Badger we are committed do doing things right. If you'd like to join the team check out our jobs page here 

Sign up to Badger News

Dominik Piatek
Software Engineer