BLOG

Can CSS Modules solve all the problems?

8/7/2018Time to read: 8 min

I might be late for the party, the concept of CSS Modules has been out for three years and I just recently started looking into it seriously. But as I read through it, I got so excited that I have to write a blog. I envisioned how it can plug into our current infrastructure and how many problems it can solve.

The problem with CSS

CSS has some major problem that people are trying to solve for years. @alexlande concludes 5 points:

  • Specificity
  • Source Order
  • Naming
  • Dead Code
  • Modification

Specificity

CSS rules run on a hard-to-pronounce concept Specificity (try saying it 10 times very fast). In order to override a rule, we need to provide a rule with higher specificity, that means, use more CSS selector, or selectors with higher specificity score, or !important. Often in a large project, when the CSS architecture is not well defined, finding a selector that can override a rule involves trial and error. Specificity also makes it harder to refactor CSS. Changing the specificity of one rule might affect other rules. It's like a rubber web, pulling one rubber band affects all others.

Source Order

The source order of CSS matters. For two rules with the same specificity, the later one overrides the former one. This is convenient sometimes, but it can lead to a lot of problems. This makes CSS more like a procedural programming language, where the format of it deceive people into viewing it as a json-like configuration file where rule orders don't matter. With the modern building tools, it's hard to guarantee a specific order of source.

Naming

CSS has a global namespace. When we define a class, we define a global variable. To name a class often requires searching through code base to make sure the new class name is not used anywhere. To make naming simpler, or to better scope the names, people have tried a lot of things, like scoping it with a root selector (which changes the specificity) or using BEM (Block Element Modifier). But they all have their own problems too. There is no easy way to truly scope CSS, unless with inline style or Shadow DOM

Dead Code and Modification

Detecting dead code in CSS is extremely hard. All the CSS rules are global, and it's hard to tell which rules are not used anymore. Together with all the problems listed above, I think we can agree that it's hard and risky to make a structural modification to CSS without a good architecture. It takes a lot of experience and understanding of the code base to make a sound decision that doesn't increase the entropy of a CSS project.


All the problems above can be solved with good CSS architecture, good practices and developers with good discipline and empathy. But not all people are CSS experts, and we often don't have a clean project to come up with. I can rant day and night about the problems in CSS, but there needs to be a solution. I think CSS Modules is a pretty good candidate.

CSS Modules

My college professor often quoted David Wheeler that "All problems in computer science can be solved by another level of indirection". Well CSS Modules is a level of indirection. The concept of CSS Modules is very simple, it changes the names in our CSS files into hashes:

.box {
  background-color: red;
}

into

._HX3elW {
  background-color: red;
}

And only those that import the CSS file (know as a module) knows the mapping from box to _HX3elW. (It sort of like having all the passwords on newspaper, but only the owner of a password knows which bank account it maps to).

How does this solve the problem? Well, it definitely solves the problem of Naming, the hashes are unique (There is a very very tiny chance that the hashes might collide, we can generally ignore the possibility in computer science).

It solves specificity, because we don't need to worry about names colliding with others, we can keep our CSS rule as flat as possible. So instead of:

.page .button { // .page is used to scope the .button
    color: orange;
}

We can just do

.button {
    color: orange;
}

We don't need to worry about .button used in other files, because they will have a different hash.

It solves Source Order to a certain degree. CSS rules in different modules won't collide, so it doesn't matter what the order is. Within one file, hopefully, things are clear and we are using source order to our advantage instead of in an unexpected way.

Using CSS Modules encourages people to break down the CSS files into smaller modules because we need to import them into JavaScript files. If we are using a component architecture, such as React. The CSS files should have a very clear connection with the JavaScript file. Usually, one component corresponds to one CSS module. This way, when a component is removed, we know it's corresponding CSS module should be removed too, thus making Dead Code elimination easy.

Because of the low specificity, smaller module, and unique naming, Modification becomes very easy. Developer's concern can be just one CSS file. We don't need to search through the codebase to see if a class is used.


The good thing about CSS Modules is that it is a specification instead of a library. A lot of popular libraries and tools have support for it. Webpack supports it; babel has plugin for it; classnames supports it; various editors support it.

Also because of its simplicity, it's very easy to come up with our own tool that simplifies the usage with it.

There are numerous tutorials online describing how to set it up, so I will leave that out.

Override Rules

One concern with CSS Modules might be, how do we override rules if we don't know the hash. This is actually a common usage. For example, see the following React code

// card.css
.card .button {
  float: right;
}

// button.css
.button {
  background: red;
  color: white;
}


// Card.js
const Card = ({ children }) => <div className="card">{children}</div>;

// Button.js
const Button = () => <button className="button">BUTTON</button>;

// App.js
ReactDOM.render(
  <Card>
    <Button />
  </Card>,
  container
);

When we use CSS Modules, the resulting code might look like:

// for simplicity, I use numbers as hash generated for CSS Modules

// card.css
.card-1 .button-1 {
  float: right;
}

// button.css
.button-2 {
  background: red;
  color: white;
}

// Card.js
import styles from "card.css"; // { 'card': 'card-1', 'button': 'button-1' }
const Card = ({ children }) => <div className={styles.card}>{children}</div>;

// Button.js
import styles from "button.css"; // { 'button': 'button-2' }
const Button = () => <button className={styles.button}>BUTTON</button>;

// App.js
ReactDOM.render(
  <Card>
    <Button />
  </Card>,
  container
);

The Button inside the Card will not get the .button-1 style. Although the souce code uses the same classname for button, but since the two rules reside in different files, they will get different hashed name. To solve this, we can provide a unhashed classname in Button:

// Button.js
import styles from "button.css"; // { 'button': 'button-2' }
const Button = () => (
  <button className={`${styles.button} button`}>BUTTON</button>
);

// card.css
.card :global(.button) {
   float: right;
}

:global means leaving the classname unhashed. So the generated CSS for card.css will be

.card-1 .button {
   float: right;
}

By doing this, we can override the child's style from the parent just as we do with plain CSS.

But the example above still has some flaws. We still need to use nested rule .card .button, which increases the specificity. And if we have the following structure:

// App.js

ReactDOM.render(
  <Card>
    <CardHeader>
      <Button />
    </CardHeader>
    <Button />
  </Card>,
  container
);

If we want to apply .card .button style to only direct Button child of Card but not the one in the CardHeader, we will have a hard time.

Override Rules, Try 2

With CSS Modules, it's recommended to pass styles object around as themes.

Let's create a public interface called styleName prop.

import styles from "button.css";

const Button = ({ styleNames }) => {
  const overrideClassNames = styleNames.button || "";
  const buttonClassNames = [styles.button, overideClassNames].join(" ");
  return <button classNames={buttonClassNames}>BUTTON</button>;
};

Parent can pass in a styles object of a CSS Module, and Button will look inside to check if it has classnames that it wants.

We can use React context to pass the style object

import styles from "card.css";
const CardThemeContext = React.createContext({});
const Card = ({ children }) => (
  <div className="card">
    <CardThemeContext.Provider value={styles}>
      {children}
    </CardThemeContext.Provider>
  </div>
);

const createThemeable = Component => props => (
  <CardThemeContext.Consumer>
    {styleNames => <Component {...props} styleNames={styleNames} />}
  </CardThemeContext.Consumer>
);

createThemeable is a function that returns a new component. The new component will be aware of the CardThemeContext.

const CardButton = createThemable(Button);

ReactDOM.render(
  <Card>
    <CardHeader>
      <Button />
    </CardHeader>
    <CardButton />
  </Card>,
  container
);

Using context and passing the styles object from Card to Button, we no longer need to nest rules and make .button global. The CSS for Card can simply be:

// card.css

.card {
  // any card styles
}
.button {
  float: right;
}

How about Selenium?

If the class names are hashed, how can automation engineer find the right selectors?

Selenium is a popular tool used for end to end testing. Automation engineers write test scripts to instruct selenium to interact the page. Selenium can do all kind of things that a normal user can do, such as clicking a button on the page, typing some words. The only thing selenium can't do is to, for example, look at the page and recognize where the login button is.

Selenium has to rely on XPath or CSS selectors to find a UI element on the page. XPath is very fragile with the dynamic nature of single page application. So CSS selectors are what most people use. With CSS Modules, the selectors are hashed, making it harder to select an element.

There are some discussion on this issue, but I don't think any approach solve the issue completely. For a DOM structure like the following:

<div class="some-other-class card--HASH"></div>

I can only think of selecting it using CSS attribute selector, like

[class*=" card--"],[class^="card--"]

This selector means: selecting any element that either contains " card--" in its classname (notice the leading space) or starts with "card--".

Conclusion

I think CSS Modules is a very interesting concept. The idea behind it is very simple: scoping our CSS with unique names that are not used elsewhere. We have been practicing this using BEM. But CSS Modules automate the process and greatly lower the effect to construct modular, concise CSS. Not everybody is a CSS architect and even for CSS architects, writing simple codes without worrying about global effects is also a pleasure.

From a simple test
A case for react-testing-library
Can we have fun in code?
my take on humors in code