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:
import styles from './Button.module.css';
: Instead of just importing the file for its side effects, we import the default export into a variable namedstyles
. Thisstyles
variable is a JavaScript object.className={styles.error}
: We access the.error
class from our stylesheet as a property on thestyles
object. We use curly braces because we are passing a JavaScript expression (the value ofstyles.error
) to theclassName
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:
- Generates Unique Class Names: It transforms your simple class name
.error
into a unique, hashed class name. The new name will look something likeButton_error__aX7yz
. This format guarantees that it won't conflict with any other.error
class in your application. - 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 inindex.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.