CSS Modules (strict mode)

Stephen Cook

At Thread, we love CSS Modules.

One issue we kept seeing was references to styles.foo long after someone removed the .foo class. This dead code seems innocent enough, but over time this bit rot adds significant noise to your code.

A more embarrassing issue I kept seeing involved long moments of trying to work out why styles['i-am-a-typo'] wasn’t applying the style I was expecting.

In response, we created strict-css-modules-loader. This webpack loader causes your styles object to log warnings whenever you attempt to access nonexistent class names.

an example console warning from strict-css-modules-loader, warning about a missing .foo class

In this article, we’ll explore a little bit about how the loader works, behind the scenes.

What Are CSS Modules?

First of all, let’s talk about what CSS Modules are. If you want an in depth explanation, you can read the CSS Modules documentation, and the css-loader documentation.

But as a very quick summary, CSS Modules allows us to write a normal CSS file:

/* styles.module.css */
.foo {
  background: red;
}

and then import this CSS file into our JavaScript file, like so:

import styles from './styles.module.css';
element.innerHTML = `<div class="${styles.foo}">`;

to give our div the .foo styles, without creating a global .foo class. Instead, behind the scenes, the .foo class gets namespaced to something like .foo-018f0s3f.

This means that each CSS file gets its own .foo class, and we don’t inherit the styles of another CSS file by mistake.

What’s the Problem?

The problem here happens when we call styles.foo, if .foo was never defined in the CSS. Rather than any sort of error, we get undefined returned as the class name.

import styles from './styles.module.css';
element.innerHTML = `<div class="${styles.foo}">`;
// same as if we wrote:
element.innerHTML = '<div class="undefined">';

There are libraries to help avoid this, but in any case, this code is costly to the developer. When trying to track down how an element is being styled, these nonexistent class names are red herrings. A developer spends the majority of their time reading code, so these red herrings are costly.

It’s worth noting that this problem isn’t specific to CSS Modules. We hit the same problem even if we hardcode our CSS classes. The underlying problem lies in there being no strong typing between the CSS file, and the JavaScript, or the DOM.

Weak typing in web development isn’t an inherently bad thing: it’s a tradeoff for speed of development, and scriptability. But in certain contexts, you want a stricter environment. This is what libraries like PropTypes give you for React props.

Ideal Scenario

If we ran code like this:

import styles from './styles.module.css';
element.innerHTML = `<div class="${styles.foo}">`;

then we would like to get a warning, if there is no .foo class defined.

But nothing currently can alert this for us. styles is a plain old JavaScript object, and if you ask it for a key that it doesn’t have, it will return undefined. No more, no less.

So how do we get our console warnings?

Naive Solution

A naive solution here would be a small function wrapper, e.g.:

import styles from './styles.module.css';
 
function getClassNameSafely(className) {
  if (!styles.hasOwnProperty(className))
    console.warn(`${className} does not exist!`);
 
  return styles[className];
}
 
element.innerHTML = `<div class="${getClassNameSafely('foo')}">`;

This would work. But it would also force us to use this new helper function every time we want to refer to a class name in our code. This would involve a large refactor, and also add annoying verbosity to our code (and size to our bundle).

Proxy Solution

What we want is the functionality of getClassNameSafely, without having to type it ourselves.

Fortunately, ES6 gives us the Proxy object. This lets us wrap a vanilla JavaScript object with added functionality on property access (e.g. getting or setting a property).

For example, it lets us write something like this:

import oldStyles from './styles.module.css';
 
function wrapStyles(styles) {
  return new Proxy(styles, {
    get(obj, prop) {
      if (!obj.hasOwnProperty(className))
        console.warn(`${className} does not exist!`);
 
      return obj[className];
    },
  });
}
 
const styles = wrapStyles(oldStyles);
element.innerHTML = `<div class="${styles.foo}">`;

This gives us the same console warning as before, but we don’t have to change our reference to styles.foo.

Proxy Loader Solution

This is almost perfect. Unfortunately, we still need to include this wrapStyles helper in our files. We want to be able to import our styling the same as we always have:

import styles from './styles.modules.css';
element.innerHTML = `<div class="${styles.foo}">`;

so the styles object given to us is already wrapped in our proxy.

To do this, we can create our own webpack loader. This sounds daunting, but webpack’s loader interface is surprisingly straightforward! You can read more about loaders, but at its core: a loader is a function that takes some input, and spits out some output.

All we want to do is take a regular styles object as input, and output it wrapped in a proxy.

// strict-css-modules-loader
module.exports = function(content) {
  // the old content will contain `module.exports = { foo: 'foo-local' }`
  // we assign the old styles object to a `rawLocals` variable instead,
  // so we can still refer to it later
  var newContent = content.replace(/module.exports = /, 'var rawLocals = ');
 
  // export the old exports, but wrapped in our proxy
  newContent += `
    module.exports = new Proxy(rawLocals, {
      get(obj, prop) {
        if (!obj.hasOwnProperty(className))
          console.warn(\`\${className} does not exist!\`);
 
        return obj[className];
      },
    });
  `;
 
  // we let webpack know we're done, and what the new content is
  this.callback(null, newContent);
};

And that’s it!

Our actual code is slightly more complex to cover some other cases, but at its core this is all it does. You can check out the source if you’d like to know more.

Using the Loader

We now have a loader that:

  • takes a plain JavaScript object, as input
  • returns a Proxy object that logs warnings, as output

To use the loader, we add it to the end of our webpack loader chain for our CSS Modules. For example, if you had:

{
  test: '\.module\.css$',
  use: [
    'style-loader',
    'css-loader?modules=true',
  ],
}

then style-loader is returning you your class name mappings. So to wrap those styles, we make our new loader run after it.

{
  test: '\.module\.css$',
  use: [
    // our new loader is first in the array, since webpack runs these loaders in reverse
    '@teamthread/strict-css-modules-loader',
 
    'style-loader',
    'css-loader?modules=true',
  ],
}

Similarly, if you were using MiniCssExtractPlugin, then we would want to move from something like:

{
  test: '\.module\.css$',
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader?modules=true',
  ],
}

to

{
  test: '\.module\.css$',
  use: [
    // our new loader is first in the array, since webpack runs these loaders in reverse
    '@teamthread/strict-css-modules-loader',
 
    MiniCssExtractPlugin.loader,
    'css-loader?modules=true',
  ],
}

Summary

The two main tools we’ve used here are ES6’s Proxy object, and webpack’s simple loader interface.

The combination of these 2 powerful things has allowed us to get detailed warnings when our code attempts to use styles that don’t exist. All by adding 1 line to our webpack config.

So join the good fight against bit rot! Give strict-css-modules-loader a go.

If you have any questions, feel free to ping me on Twitter @StephenCookDev. And if you’re looking for a job, check out our jobs page and get in touch! 🙂