Hey Bob! Your css rules break my components!

For many years, we are facing the lack of local scope in CSS, conflict happens everyday and we have to give every component a specific name like this-is-my-component.

bootstrap

css-modules is a great workaround to this problem. By sharing transformed class names between JavaScript and CSS files, we no longer worry about the global css - all class names are scoped locally!

css-modules output

The Problem

However, happiness comes with tears. As scoped css rules are tightly bound with js files, when you are trying to write a reusable component, you're possibly getting a headache: how can other developers override the default styles in my components?

We have no idea about the generated class names from outside, one thing we can do is to add some attributes onto elements so that developers can use attribute selector [attr]:

attr selector in react-toolbox

Hmm...I'd rather writing class names for every component.

Another solution is to expose an API that extends class names inside you components:

classname api

Does it mean that I may have to pass several class names in order to customize a complex component?

So, what if we could also import class name maps into sass/less/stylus ?

The Approach

A typical css-modules workflow may like this:

css-modules workflow

Code is sent to pre-processors first, then css-modules transform the css code and make exports. Before pre-processors processing, we can make some modifications to source codes - that's the possibility of creating a directive.

And right after css-modules transformation, we are able to get transformed result - that's the possibility of sharing class names.

So the solutions is:

  1. Create directives:

    1. Use some directives to interpolate class names into source code - surely it should be converted to sass/less/stylus hashes.

    2. Use variable interpolation syntax to define class name, e.g. ${map-get($comp, 'main')} in sass.

  2. Process css-modules outputs:

    1. Restore twice-transformed class names to the previous one (both in stylesheet and class map).

    2. Save the class map for interpolation.

New workflow is:

new workflow

Now we can code in this way:

Components define:

// button.js
import React from 'react';  
import styles from './button.scss';

const Button = () => (  
  <button className={style.button}>click me</button>
);

export default Button;  
// button.scss
.button {
  border-radius: 3px;
}

Your react app:

// app.js
import React from 'react';  
import './app.scss';

const App = () => (  
  <Button />
);
// app.scss
@module './button.scss' => $comp;

#{map-get($comp, 'button')} {
  background: #123;
}

Brilliant! This loader is now hosted on GitHub: idiotWu/premodules-loader.

Limitations

Since this implement is based on webpack workflow, there're some limitations with it:

  1. Must import all stylesheets you need into JavaScript code, otherwise it won't be passed through webpack.

  2. Importing files that declared with @module directives in your sass/less/stylus codes may cause breaks (pre-processors won't understand this directive).

P.S. I'm now using premodules-loader with a light implementation of themeable. You don't want build themes for using every component, do you?