Css Modules by Example

CSS Modules are a means to achieve locally scoped CSS class names.For a simple one-page app they may not be necessary. But if you're working on a large 20+ component app, this tool could really help you clean up your CSS.

Of course, adopting a new tool can take time. You might wonder:

Will I go to all the effort of setting up CSS Modules only to find out they won't work for me?

To prevent that, I've set up 7 specific examples that show how CSS Modules work in the most common situations. Skim through this article in 10 minutes and you'll know if adopting CSS Modules would help or hurt your project.

Table of Contents

Jump around if some examples interest you more than others.

example-1Example 1: Single Class Name

This is the simplest possible example. There is a single class name in the .css file: button.

Here's an overview of all the files in this example:

Widget1.css: The CSS Module. You write this like normal CSS.
Widget1.js: A React component. This one just displays a button.
Generated CSS: CSS for the browser - generated by the CSS Modules compiler.

Widget1
Widget1.css
.button {
  border-radius: 4px;
  background-color: LightCyan;
}
Generated CSS
.Widget1_button_1FUOu {
  border-radius: 4px;
  background-color: LightCyan;
}
Widget1.js
import React from 'react';
import styles from './Widget1.css';

class Widget1 extends React.Component {
  render() {
    return (
      <button className={styles.button}>
        Click Me
      </button>
    );
  }
}
export default Widget1;

Notice how the button class is renamed to Widget1_button_1FUOu. button is the local name and Widget1_button_1FUOu is the global name. You can use short, descriptive names like "button" without worrying about name conflicts.

All you have to do is write .button {...} in your CSS Module, and reference the class with styles.button in your component.

By the way, _1FUOu in Widget1_button_1FUOu is a random hash to ensure uniqueness in the case of multiple CSS Modules with the same name. You can configure how you want the CSS Modules compiler to rewrite your class names, and the hash is optional.

Detour

Before we go on to example 2, let's clarify one point that some find confusing: CSS module import statements import a JavaScript object.

import styles from 'MyComponent.css';

In the above code, the styles variable is a JavaScript object. This might be confusing if you expected to get some CSS.

To make this perfectly clear, I added a comment to each example showing what the styles variable will be set to. For example:

import styles from 'MyComponent.css';
// styles == {
//   button: "MyComponent_button_d354t"
// }

example-2 Example 2: Multiple Class Names

In this example the CSS module has 3 different class names: title, email, and submitButton.

Widget2

Email Signup


Widget2.css
.title {
  font-weight: bold;
  font-size: 16px;
}

.email {
  padding: .5rem;
}

.submitButton {
  padding: .5rem;
  margin-top: .5rem;
  border: 1px solid #2F79AD;
  border-radius: 4px;
  background-color: #6DB9EE;
}

.submitButton:hover {
  background-color: #2F79AD;
}
Generated CSS
.Widget2_title_1co1k {
  font-weight: bold;
  font-size: 16px;
}

.Widget2_email_3rJVW {
  padding: .5rem;
}

.Widget2_submitButton_1lMZk {
  padding: .5rem;
  margin-top: .5rem;
  border: 1px solid #2F79AD;
  border-radius: 4px;
  background-color: #6DB9EE;
}

.Widget2_submitButton_1lMZk:hover {
  background-color: #2F79AD;
}
Widget2.js
import React from 'react';
import styles from './Widget2.css';

class Widget2 extends React.Component {
  render() {
    return (
      <div>
        <h2 className={styles.title}>
          Email Signup
        </h2>
        <input
          className={styles.email}
          placeholder="Email Please"
          />
        <br/>
        <button
          className={styles.submitButton}
          >
          Submit
        </button>
      </div>
    );
  }
}
export default Widget2;

Notice how all three classnames became accessible to the component in the styles object. The styles object is a mapping between the class names you used, and the generated class names. It is a mapping from local to global.

example-3 Example 3: Combined Class Names

In this example there are selectors that only apply to elements which contain BOTH the button class AND the disabled class.

Notice how the .button.disabled rule becomes 2 separate variables in the styles object: styles.button and styles.disabled.

The .button.disabled CSS rule will only take affect if we use styles.button and styles.disabled on the same element. We do that in the React component.

One class in your CSS Module remains one class in the generated CSS & JS. Writing rules that combine multiple classes doesn't change that.

Widget3

Widget3.css
.button {
  padding: .5rem;
  margin-top: .5rem;
  border: 1px solid #2F79AD;
  border-radius: 4px;
  background-color: #6DB9EE;
}

.button.disabled {
  background-color: #aaa;
  border-color: #999;
}
Generated CSS
.Widget3_button_24Uk4 {
  padding: .5rem;
  margin-top: .5rem;
  border: 1px solid #2F79AD;
  border-radius: 4px;
  background-color: #6DB9EE;
}

.Widget3_button_24Uk4.Widget3_disabled_2czCZ {
  background-color: #aaa;
  border-color: #999;
}
Widget3.js
import React from 'react';
import styles from './Widget3.css';
import cx from 'classnames';

class Widget3 extends React.Component {
  render() {
    return (
      <div>
        <button
          className={styles.button}
          >
          Click Me!
        </button>
        <br/>
        <button
          className={cx(
            styles.button,
            styles.disabled
          )}
          >
          Do Not Click Me
        </button>
      </div>
    );
  }
}
export default Widget3;
We are using the classnames package here to combine classnames. In this situation it just adds a space between the two classnames.

example-4 Example 4: Nested Class Names

In this example there are style rules that only apply to elements with a particular class name that are also nested within another element with a particular class name.

Again, the classnames are returned separately. In your component, you must put one class names on a parent element, and one class name on a child.

Widget4

Widget4.css
.button {
  padding: .5rem;
  margin-top: .5rem;
  border: 1px solid #2F79AD;
  border-radius: 4px;
  background-color: #6DB9EE;
}

.fun .button {
  font-weight: bold;
  background: linear-gradient(
    90deg,
    #ff0000, #ffff00,
    #00ff00, #00ffff,
    #ff00ff, #ff0000
  );
}
Generated CSS
.Widget4_button_2Zuuj {
  padding: .5rem;
  margin-top: .5rem;
  border: 1px solid #2F79AD;
  border-radius: 4px;
  background-color: #6DB9EE;
}

.Widget4_fun_2c2WR .Widget4_button_2Zuuj {
  font-weight: bold;
  background: linear-gradient(
    90deg,
    #ff0000, #ffff00,
    #00ff00, #00ffff,
    #ff00ff, #ff0000
  );
}
Widget4.js
import React from 'react';
import styles from './Widget4.css';

class Widget4 extends React.Component {
  render() {
    return (
      <div>
        <button
          className={styles.button}>
          Regular Button
        </button>
        <br/>
        <div className={styles.fun}>
          <button
            className={styles.button}
            >
            FUN BUTTON
          </button>
        </div>
      </div>
    );
  }
}
export default Widget4;

example-5 Example 5: Composition

Obviously you are going to want to share some styles between components.

With CSS Modules, sharing is explicit using composition. We have two new files this time:

util.css: Provides a single class, grape, that we will use in Widget5.css.

Widget5
util.css
/* grapes are round and purple */
.grape {
  border: 2px solid #ff00ff;
  border-radius: 10px;
  background-color: purple;
  color: white;
}

.grape:hover {
  background-color: #ff00ff;
}
Widget5.css
.button {
  composes: grape from './util.css';
  padding: .5rem;
  margin-top: .5rem;
}
Generated CSS
.Widget5_button_1Aw-M {
  padding: .5rem;
  margin-top: .5rem;
}
Widget5.js
import React from 'react';
import styles from './Widget5.css';

class Widget5 extends React.Component {
  render() {
    return (
      <button className={styles.button}>
        Button
      </button>
    );
  }
}
export default Widget5;

example-6 Example 6: Tag Names

CSS Modules rewrite class names, but they don't touch tag names.

Selectors like input, div, or h1 will be left alone.

In a selector like .signup input, the signup class will be renamed, but the input portion of the selector will be left alone and you get something like this: .filename_signup_123 input.

May I suggest: Never use a selector that is JUST a tag name. Always specify a class name also. Otherwise the CSS rule will apply globally to your entire app.

Widget6
Widget6.css
input.large {
  font-size: 20px;
}

.medium input {
  font-size: 14px;
}

.tiny * {
  font-size: 9px;
}

/* Don't do this!
   This style will be global. */
input {
  padding: 0.5rem;
}
Generated CSS
input.Widget6_large_1QsrJ {
  font-size: 20px;
}

.Widget6_medium_FwRDZ input {
  font-size: 14px;
}

.Widget6_tiny_2iJZp * {
  font-size: 9px;
}

/* Don't do this!
   This style will be global. */
input {
  padding: 0.5rem;
}
Widget6.js
import React from 'react';
import styles from './Widget6.css';

class Widget6 extends React.Component {
  render() {
    return (
      <div>
        <input
          className={styles.large}
          placeholder="I am large"
          />
        <div className={styles.medium}>
          <input placeholder="I am medium" />
        </div>
        <div className={styles.tiny}>
          <input placeholder="I am so tiny" />
        </div>
      </div>
    );
  }
}
export default Widget6;

example-7 Example 7: Media Queries

Since all CSS Modules do is rewrite your classnames, the features you are used to in CSS still work as-is.

Here's an example that uses media queries. Different text is highlighted depending on your window width.

Widget7
The Window is Small
The Window is Big
Widget7.css
.small {
  opacity: 0.2;
}
.large {
  opacity: 1.0;
}

@media (max-width: 600px) {
  .small {
    opacity: 1.0;
  }
  .large {
    opacity: 0.2;
  }
}
Generated CSS
.Widget7_small_2tsQ- {
  opacity: 0.2;
}

.Widget7_large_1NLxE {
  opacity: 1.0;
}

@media (max-width: 600px) {
  .Widget7_small_2tsQ- {
    opacity: 1.0;
  }
  .Widget7_large_1NLxE {
    opacity: 0.2;
  }
}
Widget7.js
import React from 'react';
import styles from './Widget7.css';

class Widget7 extends React.Component {
  render() {
    return (
      <div>
        <div className={styles.small}>
          The Window is Small
        </div>
        <div className={styles.large}>
          The Window is Big
        </div>
      </div>
    );
  }
}
export default Widget7;

Notice that only the class names are changed. Your media queries stayed exactly how you wrote them.

Conclusion

Regardless of whether or not CSS Modules will be helpful to you - now you know. If you do decide to use CSS Modules, there should be no surprises.