Architecting CSS for Responsive Large Scale Applications

Filed under:

Photo by Joel Filipe on Unsplash.

CSS is often regarded as a second-class citizen in web development. It doesn't always get the same level of attention as JavaScript does. The problem is that CSS is flat, global and unstructured by nature, so developers need to create and enforce systematic conventions for writing it. Many developers are opinionated and under tight deadlines, so without an agreed set of rules the team commits to, CSS can spiral out of control very quickly. A good design system provides meaning for designers and developers, making development easier and more maintainable in the long term. It's almost like building with LEGO bricks!

I've adapted the way I write CSS many times during the years. In recent years I've settled with a hybrid set of methodologies that work well for me and the team, so I thought I'd document my approach here. A few years ago people released new JavaScript MVC frameworks almost on a daily basis. Now the same is happening to CSS: new CSS frameworks and toolkits are released regularly, which is a sign people actually care!

Problems we're trying to solve

Some of the problems in CSS land:

  • Selectors mixing layout and look & feel, doing too much
  • Nested selectors tied to their location in the DOM, reducing scope for reuse (contextual styling)
  • CSS and JavaScript tightly coupled
  • Design patterns not abstracted enough
  • Specificity issues
  • Different developer preferences
  • Time spent in debugging/fixing issues due to style leaking

Many articles have been written on the topic, I'm just restating the problems here to explain how I arrived at my favorite setup. And no, vjeux's CSS-in-JS, driven mostly by ReactJS folks, does not provide all the answers to these problems. More on that later.

Requirements for scalable CSS design system

According to my experience, these are the main requirements for a scalable CSS architecture that minimizes side effects:

  • Components should be able to live anywhere in the DOM and in the layout, without things breaking or styles leaking
  • Components should be open for extension
  • Each component should be responsible for a single part of the UI
  • Components should be composable (like LEGO bricks), for building bigger components and pages
  • And finally, the system should communicate useful information to developers in class names when reading a DOM snippet.

With those requirements in place, let's see how we can achieve this.

Methodologies

I use a mixture of SMACSS (splitting the rules across base, layout, component and state layers), BEM (component-level naming conventions) and OOCSS (abstracted reusable patterns and utilities). The reason why I like these methodologies is that they define a high-level abstraction, leaving the actual implementation and syntax details to the team to decide.

Base

This layer contains only all base element defaults and reset rules, no classes/IDs. You should never use IDs for styling anyway.

Layout

This layer contains the major layout and grid rules, with classes always prefixed with .layout- or just .l-. These classes may be used also on component level if necessary, to create minor layout in components. In this case, they can live in the same DOM element than the component top-level namespace but again, prefixed with .l-.

Component and modifier

Components should rely on their parent container for sizing. They should only know how to style themselves and do that job well. The less the components know about their surrounding elements or their external spacing, the better. Components can be composed of other components but they should inherit rules only from the base, not from other components. I've used BEM many years now (Harry's Mindbemding article is recommended reading).

Component state

State classes should be prefixed with .is- or .has-, and scoped to the component or sub-element and never global, for example .tweet.is-favorited. Media queries also modify component state.

So what's the difference between a state and a modifier? It can be difficult to decide sometimes. The difference is subtle: state classes describe component's appearance in various states, whereas BEM modifier classes describe modifications in component's base appearance, regardless of state. Very subtle (and somewhat theoretical).

Utils

This is where lines again become blurred. What's the difference between a component and a utility? In my mind, component is a namespaced singleton whereas utility is common functionality or atomic property that is applied to many objects (components). How you want to call these is up to you and your team. Some people prefix utility classes with .u- to indicate their purpose.

I use utility classes mostly for managing white space (such as .ml1 which would be defined as {margin-left: $white-space1;} in Sass for example), but they can be basically anything. Just remember they have to be different from layout and component namespace classes. Spacing classes is a feature that's missing from many popular frameworks, which is why I prefer smaller, modular and designless toolkits. I particularly like Scut, SUIT CSS, Basscss and Tachyons, to name a few. They provide useful pieces of reusable functionality, either as simple atomic classes doing one thing, or larger mixins implementing certain feature (for example centering an element on both axes). I'm also using the OOCSS media object extensively.

Basic example

See how much meaningful information you can extract from the markup snippet below. It demonstrates the use of all the class layers described above.

<pre><code><div class="l-row">
  <div class="l-row__unit l-row__unit-3">
    <div class="accordion" id="js-accordion" data-ui-component="accordion-stream" aria-multiselectable="true">
      <h3 class="accordion__heading is-expanded" role="presentation">
        <a href="#js-acc1-p1" id="js-acc1-t1" role="tab" aria-selected="true" aria-controls="js-acc1-p1" tabindex="0">Tab heading</a>
      </h3>
      <div class="accordion__panel media" id="js-acc1-p1" aria-labelledby="js-acc1-t1" role="tabpanel" aria-hidden="false">
        <div class="media__img">
          <img width="150" src="" alt="" />
        </div>
        <div class="media__body u-text-truncate">
          <h2 class="h1 ml1 mr1">Module Headline</h2>
          <p>The media object saves hundreds of lines of code.</p>
        </div>
      </div>
    </div>
  </div>
</div>
</code></pre>

Responsive design and exception handling

Now let's throw responsive design into the mix. How do you apply a layout, media object or utility only for certain screen sizes? What if you sometimes want that class to be responsive, and sometimes not? As always, it depends.

Let's take media object as an example. Consider these design scenarios:

  • I want the media object to be responsive all the way from small to large screens
  • I want the media object layout to be applied only on, say, large screens
  • I want to keep the same media object layout on all screens sizes

How would you manage these three situations? It's helpful if we can identify the default case first. What do you think the default rule should be? What are the exceptions here? I'd argue that since we want to use mobile first approach, responsive should be the default, and if we want to override that, we'd use a modifier. Here then the default class would be media-object (with media queries), and for the second case, we'd add media-object--page-lg that would override relevant rules up until large screen. For the third case, we'd use media-object--page-xs and add our overrides there. Since responsive is the default, overrides would only be rare edge cases: exceptions. Alternatively, we could use state classes since media queries belong to component state domain, like so: .media-object.is-page-xs. Up to you, as long as you do it systematically in your team.

Managing variations

The caveat here is that at some point, you'll realize you have too many minor component variations. This is not a failure in code, it's a failure in design system. The solution is two-fold: talk to the designers, and use Style Guide Driven Development. Your future self, your colleagues and your users will thank you.

Contextual styling

Also worth reminding is that contextual styling should be kept to a minimum. A component may be designed to look different inside another component, but that doesn't mean that it should be implemented with contextual styling. There is no nested component context, and that is simply because components don't know of each other. A component can be positioned inside another component, but that responsibility belongs to minor layout classes (or utilities). Nested components' job is simply to render themselves. If a component happens to look different when it's inside another component, it's because that nested component is using a component modifier, not because it's inside another component. When you understand this, you'll experience the full power of scalable CSS components, modifiers and utilities.

The future

The approach I've described in this post has worked very well for me and my team the last few years. We've had very few style leaks and practically no UI regressions. I mentioned in the beginning that I'm not a fan of CSS-in-JS. It's an interesting experiment and an honest try to solve the issues CSS has, but it feels a bit over-engineered and too tightly coupled with JavaScript, solving some of the problems but creating others. Some argue that CSS classes should only define the stateless appearance of a component, while state styles should be inlined, to encapsulate behaviour in one place. Fair enough, good point, but if you start putting styles inline, where do you stop? You have to go all the way, otherwise it gets too messy. I'm not completely against it, I'm just not convinced that CSS-in-JS would support designer/developer workflow that well, not to mention backend developers who also touch frontend code. Maybe it works well in certain projects. Keep in mind though that it locks you down to your selected JavaScript framework.

With that said, I find CSS Modules concept quite exciting and will be following it very closely going forward. Best practices change all the time. You decide what's best for your team and product. It's best to keep your mind open.

Tools

This post is not about tooling, but suffice to say that you should of course be using stylelint and PostCSS with your Sass. With stylelint, you can programmatically enforce BEM syntax in your components, prevent rule nesting, and do lots of other things. I recently wrote an article about configuring stylelint.

Closing thoughts

Many backend developers and designers I've worked with also like this layered class-based approach because it empowers them. Changing a few atomic class attributes in markup is far easier and faster than refactoring tangled style sheets. On the other hand, if the markup contains too many atomic classes, you might be giving too much freedom to markup authors. Make sure to keep the UI consistent. That's why you're doing Style Guide Driven Development, right?

Before adding anything new to the codebase, do yourself and your team a favor and ask yourself the following questions:

  • Does this style exist already? If yes, can I reuse it and maybe abstract it into a utility?
  • If this style doesn't exist yet, what should the new class do? Does it belong to layout, component, state or utility domain? Or is it just a hook for JavaScript, without styling? If it's none of these, fall back to global HTML elements.

The goal here is to use methodical approach, to reduce ad hoc decision-making.

In this post I've talked a lot about components. In many ways, components, modifiers and functional utilities are the meat of the matter of reusable and scalable CSS. It's important to keep in mind though that component-based design doesn't always mean that all the CSS should live in the dedicated CSS file for that component (most of it should though). A composed HTML component can include classes from different layers as long as it's very clear what each class does (because you have a self-documenting naming convention described in this post and shown in the basic example snippet). Go compose!

Partial credits

I'm listing here just a few epic articles. You can find many more in my frontend-dev-bookmarks.