Skip to main content

CSS Modules (Part 1): Scoped Styles #27

📖 Introduction

In our previous articles, we explored styling with plain CSS files and dynamic inline styles. We learned that plain CSS is global by nature, which can lead to class name collisions in large projects, and that inline styles can't handle features like pseudo-selectors.

This article introduces a powerful solution that offers the best of both worlds: CSS Modules. CSS Modules look like regular CSS files but are transformed at build time to scope class names locally to the component they are imported into, elegantly solving the problem of global scope.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • Importing CSS in React: You should be comfortable with the import './styles.css'; syntax.
  • The Global Scope Problem: A clear understanding that standard CSS can lead to class name conflicts.
  • JavaScript Objects: Familiarity with accessing object properties using dot notation (styles.myClass) and bracket notation (styles['my-class']).

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Problem CSS Modules Solve: A clear explanation of the global scope issue in CSS.
  • What CSS Modules Are: Understanding that a CSS Module is a regular CSS file that is scoped locally by default.
  • The .module.css Convention: How to name your files to enable the CSS Modules transformation.
  • Importing and Using Scoped Styles: The syntax for importing and applying styles from a CSS Module.
  • How It Works: Seeing how class names are automatically made unique during the build process.

🧠 Section 1: The Core Concept: Escaping the Global Scope

Imagine two different developers working on two different components: a Button and a Card. Both developers might independently decide to create a class named .error.

Button.css

.error {
background-color: red;
color: white;
}

Card.css

.error {
border: 1px solid red;
color: red;
}

If both of these standard CSS files are imported into the application, they will clash. The styles from the file that is imported last will override the other, leading to unpredictable and buggy UIs.

CSS Modules solve this problem. By default, every class name you write in a CSS Module is scoped locally to the specific component that imports it. This means you can safely reuse simple, descriptive class names like .error, .title, or .wrapper in every component's stylesheet without ever worrying about them conflicting.


💻 Section 2: How to Use CSS Modules

Using CSS Modules is incredibly simple in modern React toolchains like Vite or Create React App. It's all based on a special file naming convention.

2.1 - The .module.css Naming Convention

To turn a regular CSS file into a CSS Module, you simply change its extension from .css to .module.css.

Let's create a new Button component. We'll create two files in its folder:

  • Button.jsx
  • Button.module.css

2.2 - Writing the Styles

The CSS inside Button.module.css is just plain CSS. There's no special syntax to learn.

/* Button.module.css */
.error {
background-color: red;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
}

2.3 - Importing and Using the Styles

This is where the magic happens. When you import a .module.css file, you import it as an object (conventionally named styles). The keys of this object correspond to the class names you wrote, and the values are the unique, generated class names that React will use.

// Button.jsx
import React from 'react';
// Step 1: Import the module stylesheet and name the import 'styles'
import styles from './Button.module.css';

function Button() {
// Step 2: Use the 'styles' object to access your class names
return (
<button className={styles.error}>
Error Button
</button>
);
}

export default Button;

Code Breakdown:

  1. import styles from './Button.module.css';: Instead of just importing the file for its side effects, we import the default export into a variable named styles. This styles variable is a JavaScript object.
  2. className={styles.error}: We access the .error class from our stylesheet as a property on the styles object. We use curly braces because we are passing a JavaScript expression (the value of styles.error) to the className prop.

🛠️ Section 3: How It Works Under the Hood

When your application is built, the CSS Modules processor runs. It takes your Button.module.css file and does two things:

  1. Generates Unique Class Names: It transforms your simple class name .error into a unique, hashed class name. The new name will look something like Button_error__aX7yz. This format guarantees that it won't conflict with any other .error class in your application.
  2. Creates the styles Object: It creates the JavaScript object that you import, mapping your original class names to the new, unique ones.

The styles object you import will look like this:

{
error: 'Button_error__aX7yz'
}

So, when your component renders <button className={styles.error}>, React is actually rendering <button class="Button_error__aX7yz">. This is how CSS Modules achieve local scope.


✨ Section 4: Best Practices

  • Use .module.css for Component Styles: This should be your default choice for styling individual components. It provides the safety of local scope with the full power of CSS.
  • Use .css for Global Styles: Continue to use regular .css files for global styles (like in index.css) that you want to apply everywhere.
  • Use camelCase for Class Names: While you can use kebab-case (.my-class) in your CSS file, you will have to access it with bracket notation in your JSX (styles['my-class']). It's often easier to use camelCase (.myClass) in your CSS so you can use dot notation (styles.myClass) in your JSX.

💡 Conclusion & Key Takeaways

CSS Modules offer a powerful and intuitive solution to the biggest problem in CSS: managing the global scope. By scoping styles locally to components by default, they allow you to write simple, reusable class names without fear of conflicts.

Let's summarize the key takeaways:

  • Local by Default: CSS Modules make your styles local, not global, preventing unintended side effects.
  • The .module.css Convention: Simply renaming your file is all it takes to enable this feature in most modern React setups.
  • Import as an Object: You import styles from a CSS Module as a JavaScript object and use its properties to apply classes.
  • No More Naming Conflicts: You can confidently reuse class names like .wrapper or .title in every component's stylesheet.

Challenge Yourself: Create an Input component with its own Input.module.css file. Define a .input class and a .withError class in the stylesheet. In your component, use the useState hook to track whether the input has an error. Conditionally apply both the .input and .withError classes when the error state is true. (Hint: you'll need to combine two properties from the styles object into the className string).


➡️ Next Steps

You've now learned the fundamentals of CSS Modules. In the next article, "CSS Modules (Part 2): Composing classes and using global styles", we'll explore more advanced features, including how to combine multiple classes and how to intentionally use global classes from within a CSS Module.

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • CSS Modules: A system that automatically transforms CSS files at build time to scope class names locally. This is achieved by generating a unique hash for each class name.
  • Local Scope: In CSS Modules, this means a class name defined in one file is not accessible to other files unless explicitly composed.
  • Global Scope: The default behavior of CSS, where any style can potentially affect any element on the page, regardless of where the style is defined.

Further Reading