Thinking

You’ve got a Destination. We’ve got the Road Map.

CSS modules: first look

CSS modules::before

CSS (Cascading Style Sheets) was initially released in 1996 in the midst of the browser wars—when Internet Explorer 3 and Netscape Navigator 4 were fighting to control the web.

Typically, if you wanted to style a web page back then, you'd have to use the limited styling capabilities of HTML 2.0. CSS was a new robust styling solution that would separate the presentation from the content.

One of CSS’s largest advantages (and, let’s face it, disadvantages) is the way rules cascade from parent to child. To put it in reverse, more specific rules inherit from their more general parents, but I suppose “Inheriting Stylesheets” wasn’t as catchy.

The biggest benefit of cascading is the tendency to avoid duplication. Changing a property in a parent rule set will automatically affect all of its children. This works well for small projects. It’s easy to understand the language of CSS, and it’s not difficult to trace the inheritance, especially if one is using a browser’s built-in debugger.

Anyone who’s worked on a larger project (especially one they’ve taken over from a previous developer) likely knows the pains of trying to modify existing rules. As I said, changing a property in a parent rule set will automatically affect all of its children. A small change in one rule set can have unintended consequences on seemingly unrelated pages. This leads to hacks, such as long chains of classes on elements, complex selectors to game the rules of specificity, and generous usage of the !important annotation.

Over the years, we attempted to compensate for these issues by creating new pseudo-languages like LESS and SASS so that we programmers can essentially write some code that will transpile down to CSS. While this does solve many issues, in the end, you’re still left with plain old CSS filled with !important annotations. (Incidentally, use of CSS modules doesn’t preclude use of a preprocessor. You could use CSS modules written in SCSS, for example.)

CSS naming conventions like BEM and OOCSS attempt to tame CSS’s cascading ability by limiting nesting and adding namespacing. They put the onus on you to maintain scheme, which can be burdensome while adding more and more rules over time.

Alternatively, you can deconstruct your rule declarations into individual styles and put them in an element’s inline style attribute, such as with React inline styles and Atomic CSS. This bypasses cascading altogether, but can potentially create large HTML files with repetitive style declarations.

I’ve always felt that CSS’s cascading ability is one of its greatest assets. It just needs to be coaxed into working for us instead of against us.

What are CSS modules?

To be fair, CSS modules appear to be yet another layer of abstraction that ends up transpiling down to vanilla CSS. Maybe so, but CSS modules do solve one of the primary pain points of CSS. In short, CSS modules isolate your CSS rule sets from each other while still allowing them to inherit from other rule sets within the same module (but not necessarily parent rule sets).

CSS modules are defined by a few main tenets. First, every rule set is scoped locally by default. Second, modules prefer composition over inheritance. Third, elements should never have more than one class applied at a time.

Locally-scoped styles

The app I’m building needs some buttons. I’ll define some rule sets for green primary buttons and red cancel buttons:

Pretty standard stuff. Now, here’s the equivalent CSS module:

The magic happens when we run this through a transpiler, such as Webpack, with the appropriate loader. We don’t need to simulate namespacing by specifying an overall parent element anymore, because the parent will be the name of the file. Here’s how we would use this in a React component:

Instead of applying the properties to the inline style attribute, Webpack will take the rule sets that have been loaded and produce a namespaced version. It will then replace button.primary and button.cancel in our JavaScript with the newly generated class name. Our HTML output would look something like:

And our generated CSS would look like:

You’ll notice the exported rule sets in the example are not nested. Like with BEM, you won’t have to worry about the rules of specificity anymore because every selector has the same specificity (in this case 0, 0, 1, 0).

At this point, you might be wondering what’s been gained. After all, if I change buttons.css and rebuild my app, every generated style will also change accordingly. So far, all we’ve done is overcomplicate CSS.

Composition

Now I want to make large and small versions of my primary and cancel buttons. In vanilla CSS, I might do this by defining rules common to a button, then additional rules for specific sizes, then rules for primary and cancel. It might look like this:

Okay, not bad. What if I want to add another size? Or another color?Or another vector, like whether the button is enabled? Now it’s going to get more complicated to define and to maintain. Luckily, with CSS modules, we can define the basic button components and compose them into the buttons we need.

Above, I described the components I’d need, and then translated them into CSS. Instead, I’ll just define the components by themselves. I could start by defining a small set of rules common to all buttons:

Then some button sizes:

And some button colors:

And finally, I’ll compose my buttons:

I’ve created a set of reusable components that have been composed together like a great CSS Voltron. If one wanted to be really future-proof, it’s best to take the time to define many single-responsibility components that each contain as few rules as possible. That way, new rule sets can be created by composing existing rule sets in different ways, instead of trying to override existing rules in the inheritance hierarchy.

In vanilla CSS, when we come across an element that needs a one-off, our best choice is to apply the closest existing class to the element, and then create another class to bend the existing class to our needs. We apply both classes to our element. If we do this enough, we’ll end up with a bunch of small rule sets all modifying different properties of a single base rule set. This fragile situation leaves us open to disaster if someone unwittingly modifies the base rule set.

However, with CSS modules, I just assemble the components for the class I need. If the components are small and have a single responsibility, it’s less likely that the components will need to be modified in the future. If I need to change my composed class, I just switch out which components it’s using or modify its additional rules.

If you’re familiar with CSS preprocessors, you’re probably thinking that this looks a lot like the “extend” functionality. From a programmer’s perspective, you can think of them similarly. However, the output of each is quite different. Preprocessors actually copy the referenced rules and selectors creating self-contained rule sets. CSS modules remain separate, but each composing class is added to the element you’re styling.

One class per element

In React, if you want to apply more than one style to an element, you have a few options. In vanilla CSS, you can simply add more than one class name to an element. Using CSS modules, you could import several classes, join them, and apply the result to an element. This is not recommended.

When using CSS modules, the best practice is to compose your rule sets in CSS files and apply them to the element as a single class. Let me be clear: in your code, there should only be one class per element. After transpilation, there will be several classes applied to the element (if your rule set uses composition).

Continuing with my previous example, I’ll create an element that renders a large, green primary button and a small, red cancel button:

The HTML output looks like:

Every class I imported was given its namespaced selector, and the ones that compose my large and small buttons were appropriately applied to my elements. My exported CSS looks like:

Unfortunately, there are some shortcomings here. First, my main rule sets are empty because all they did was compose other rule sets and didn’t add any additional rules. Second, even though we didn’t use the large cancel button or the small primary button, they’re still included in the output. Unfortunately, the way CSS modules are currently processed, it’s difficult to exclude unused rule sets from the output. Dead code elimination is a handy feature that’s currently being discussed, but in the meantime, there are some existing techniques (hyperlink to https://github.com/purifycss/purifycss-webpack-plugin) for eliminating unused and empty classes.

Global rules

Modules are awesome, but there may still be situations where one would want to apply rules globally.

You can switch between local and global scope by using the :local and :global pseudo-selectors. You can switch the scope at any point in a selector. This applies only to class and id selectors. Element selectors are always global. This will apply rules globally to any element with the title class and any local elements with the avatar class inside any element with the heading class.

That will transpile into:

Animations

Animations can also be locally scoped and composed. Here, I’m defining an animation and applying it to a rule set of the same name. I can now compose that animation into another rule set to add the animation:

This becomes:

As shown in a previous example, when I apply the flashy-text class to an element, the blue-green-text class will also be applied automatically.

While I did include all of these in a single file for this example, a common practice is to keep animations in one CSS file along with identically-named rule sets. You could even separate animations into their own modules based on the type of element they are intended to animate.

Follow