How can we help?

Design? Check. Strategy? Check. Bulletproof code? Check! People who can manage it in an agile and efficient manner? Check. Someone to help you create your next big product? Of course.

Denver

- 1201 18th St. Suite 250, Denver, CO 80202

Grand Rapids

- 125 Ottawa Ave NW Suite 270, Grand Rapids, MI 49503

Blog

BLOG:CSS Modules  -  Solving the Challenges of CSS at Scale

CSS Modules - Solving the Challenges of CSS at Scale

Let’s take a quick stroll down memory lane. The year is 1996. The web is not mainstream and content is still largely document-based. (Eeeks, remember those days?!) Stylesheets are tiny (by 2016 standards, that is) and any maintenance burden is easily offset by the tremendous benefit of decoupled styling. CSS1 has just released, and the intent of the specification is to separate the presentation of a document from the document content.

Fast forward to today. Today we almost never think of content in the context of a document. Content is dominated by highly dynamic web applications with graphically intensive stylings. Our production stylesheets easily reach into the thousands of lines of code with several hundred selectors. It’s no wonder building and maintaining CSS at this scale presents significant challenges for development teams. Overcoming these challenges requires nothing short of a perfect balance of discipline, tooling and frameworks. Let’s dive in and figure out how these challenges are overcome, shall we?

The challenges of CSS at scale

At Universal Mind, we’ve been architecting and building large web applications for well over a decade. We’re very familiar with the unique challenges of CSS at scale, both in development and in production.

Global Scope

CSS has one implicit, global scope.

We train developers regarding the dangers of global scope in JavaScript, yet we overlook that same danger with CSS.

Regardless of which framework, pre-processor or build system you’re using, application CSS will execute at runtime in a single global scope. As a result, selector collisions and unintentional cascade will eventually creep in, leading to lost development velocity, cross-platform UI rendering issues and developer anxiety in changing an already fragile stylesheet.

Deeply nested overqualified selectors

As developers attempt to mitigate global scope issues, they will start to over qualify selectors in an attempt to create a pseudo-scope. Hate to break it to you, but this never works out well.

.widget table row cell .content .header .title {
padding: 10px 20px;
font-weight: bold;
font-size: 2rem;
}

Overqualified selectors have several issues

  • They’re a performance nightmare. This small example would require the browser to make 7 fetch attempts across the DOM before rendering. The implications of a single selector are negligible, but multiply this over an entire codebase and you can inadvertently add seconds to your page rendering time.
  • They add a lot of unnecessary weight to your site. Byte count still matters, especially on mobile where data speeds are limited.
  • They limit reusability. Instead of working with the cascade, they’re fighting against it, limiting reuse and promoting duplication.

Refactoring Once your app is deployed, how will you implement iterative changes to the styling? Is there a clean way for you to determine which selectors will affect your component? How will you identify dead code? Your CSS architecture should encourage encapsulation and promote easily maintainable code.

Steps in the Right Direction

There are several frameworks and methodologies in use today that address some of these challenges, but none of them individually solve every challenge, and each has its own challenges.

Preprocessors (Sass, Less etc.)

CSS pre-processors have been around for the better part of the last decade. They’re a valuable part of any CSS architecture and address some (though not all) of the challenges inherent in CSS at scale.

  • Code maintainability improves dramatically with imports, values and mixins.
  • Refactoring becomes less scary and more predictable as module specific styling is encapsulated in its own relative file.

Despite the many benefits of pre-processors, their greatest contributions improve development and don’t mitigate the greatest run-time issue, global scope.

A typical Sass root file

// root.scss
@import 'reset';
@import 'global-values';
@import 'header';
@import 'item-list';
@import 'footer';

Pre-processors use import to reassemble modules prior to minification, so, in the end, you’re still placing all of your selectors in the global scope. This doesn’t solve the global scope challenge.

BEM (Block Element Modifier) BEM methodology advocates modularity in CSS through the use of selector naming conventions.

[block]__[element]--[modifier] {
padding: 10px 20px;
font-weight: bold;
font-size: 2rem;
}
  • Block — Standalone entity that is meaningful on its own. (header, container, menu, input)
  • Element — Parts of a block and have no standalone meaning. They are semantically tied to its block. (menu item, list item, checkbox caption, header title)
  • Modifier — Flags on blocks or elements. Use them to change appearance or behavior. (disabled, active, checked, big, red, error)

Used properly, BEM is a sound approach to creating modular, reusable and structured CSS. It’s capable of solving the global scope challenge, and when combined with a preprocessor (like Sass), you have an approach that successfully addresses many of the challenges of CSS at scale. However, it’s not without its own issues.

  • Deeply nested elements can quickly lead to unruly selector names and require a lot of cognitive effort.
  • For BEM to work, you must be consistent in your implementation of the naming conventions. For large teams, this can be difficult to enforce.

There’s a better way…

CSS Modules

CSS Modules deliver the best of both worlds. They preserve everything we know and love about CSS and layer in component architecture paradigms that we’re already in love with.

They generate locally scoped class names that are easy to reason about, without introducing complex conventions.

Creating a CSS Module is no different than creating any other CSS file. With CSS Modules, you’re free to name your classes whatever you like, without fear of global scope issues. No more complicated BEM names. The CSS syntax is unchanged, so all of your existing tooling will continue to work as expected.

An example CSS module

/* components/demo/ScopedSelectors.css */
.root {
border-width: 2px;
border-style: solid;
border-color: #777;
padding: 0 20px;
margin: 0 6px;
max-width: 400px;
}
.text {
color: #777;
font-size: 24px;
font-family: helvetica, arial, sans-serif;
font-weight: 600;
}

Loading a CSS module into the local scope of your component is as simple as using require or import, just like you would any other JavaScript module

/* components/demo/ScopedSelectors.js */
import styles from './ScopedSelectors.css';

Wait, what!? You can require CSS? We’ll get to that in a bit.

Once your module is loaded you can reference your CSS class names like you would any other property.

A simple React example

import React, { Component } from 'react';
import styles from './ScopedSelectors.css';
export default class ScopedSelectors extends Component {
render() {
return (
<div className={styles.root}>
<p className={styles.text}>Scoped Selectors</p>
</div>
);
}
};

Notice we reference the “active” class from our module using dot-notation. Referencing a loaded CSS module is the same as any other JavaScript object.

All this is made possible by the CSS Module loader, which uses require or import (common JavaScript paradigms) to compile your CSS and attach it to the local scope of your module, with globally unique class names.

Using our React example above, our rendered output would look something like this

<div class=”ScopedSelectors__root___16yOh”>
<p class=”ScopedSelectors__text___1hOhe”>Scoped Selectors</p></div>

The module loader has transformed {styles.root} and {styles.text} into globally unique class names. Resulting in styles that are unique and local to your component. The generated class names use a form of BEM notation which includes the component name, class name and a unique hash, ensuring the class name is globally unique, even if you have two classes with the same name in separate modules.

Promoting reuse through composition

Efficient class reuse is critical to minimizing duplication and maintaining a consistent UI in your app. CSS Modules facilitate class reuse through composition. One class can “compose” one or more other classes.

Considering our previous example, we could easily abstract the general layout and typography attributes of both classes into higher level files, thereby promoting reuse in other components within the application.

/* components/demo/ScopedSelectors.css */
.root {
composes: box from "shared/styles/layout.css";
border-color: red;
}
.text {
composes: heading from "shared/styles/typography.css";
color: red;
}

The layout file is another CSS module

/* shared/styles/layout.css */
.box {
border-width: 2px;
border-style: solid;
padding: 0 20px;
margin: 0 6px;
max-width: 400px;
}

as is the typography module

/* shared/styles/typography.css */
.heading {
font-size: 24px;
font-family: helvetica, arial, sans-serif;
font-weight: 600;
}

Working with Pre-Processors

CSS Modules do not exclude using a pre-processor. If you’re using a tool like WebPack, using Sass with CSS Modules is very simple.

Are CSS Modules right for my project?

CSS Modules are not suitable for every project. A component based architecture is required (React, Angular 2), and aside from Rails, they only work with JavaScript applications. Furthermore, if you’re already using BEM, it may not make sense to switch to CSS Modules.

For your next project, large or small, I would strongly recommend you consider using CSS Modules. Currently supported loaders include Webpack,JSPM, Browserify and there’s also a WIP plugin for Rails.

Conclusion

Looking back at our three challenges of CSS at scale, CSS Modules solve them all and add much-needed structure to our projects.

  • Global Scope — CSS Modules eliminate collisions in the global scope, by leveraging uniquely generated class names using a modified BEM notation.
  • Overqualified Selectors — Because CSS modules live at the component level, there’s no need to write deeply nested selectors. Class names can be kept simple and relevant to the component.
  • Refactoring — Refactoring is made simple, because we’re working at the component level, we can easily determine which styles apply to the component. For styles that use composition, we can quickly locate the other affected components.

1996 is behind us, as is our stroll down memory lane. Content is dynamic. Stylesheets are enormous. Finding a solution to building and maintaining CSS at such a large scale is crucial. CSS Modules just might be that solution.