In the previous article, we saw that one of the most common challenges is managing the global scope of styles without falling into naming conflicts.
That is, if you name a class .card in one place, it applies everywhere. This forces you to use mile-long names like .Sidebar__UserProfile--active to avoid collisions.
This is where CSS Modules comes into play, a technique that allows you to encapsulate styles at the component level, preventing one component’s styles from affecting others.
What are CSS Modules?
A CSS Module is not a library you need to install, nor a new and strange syntax. It is simply a normal CSS file where all classes are locally scoped by default.
If the file ends in .module.css, Vite will treat it as a module.
Why does this work? Thanks to a naming convention and the build process of tools like Vite.
Step-by-step Implementation
To see its use in an example, let’s refactor our Button component to use modules.
src/
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ ├── Button.module.css
The common convention is to name style files with the .module.css extension.
We create the file Button.module.css. Note the extension. Inside, we write standard CSS, but now we can use generic names without fear.
/* Button.module.css */
.button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border-radius: 5px;
}
.error {
background-color: red;
}
Here is the biggest difference. We no longer import the file “in the air” (import './file.css'). Now we import it as an object, usually called styles.
// Button.jsx
import styles from './Button.module.css'; // We import the object
export default function Button() {
return (
// We access classes as properties of the object
<button className={styles.button}>
Click me
</button>
);
}
- Zero conflicts: You can stop worrying about whether
.containeralready exists somewhere else. - Maintenance: If you delete the
Button.jsxcomponent, you can deleteButton.module.csswith the certainty that you weren’t breaking styles in another part of the website. - Explicit code: When reading
styles.title, you know exactly where that style comes from.
- Syntax: You have to write
styles.in front of everything. - Less reuse: You can’t rely on global utility classes unless you import them explicitly.
CamelCase vs Kebab-case
In CSS we usually use hyphens (.btn-primary). However, in JavaScript, hyphens are not valid for accessing properties with a dot (styles.btn-primary ❌).
If you use hyphens in your CSS Module, you will have to use bracket notation:
<button className={styles['btn-primary']}>
This is a bit awkward. That’s why, when working with CSS Modules, it’s a good practice to write classes in camelCase:
/* Preferable in Modules */
.btnPrimary { ... }
/* Much cleaner */
<button className={styles.btnPrimary}>
Combining Classes (Composition)
What if we want to apply multiple classes? Since styles.class returns a string, we can use the same techniques we saw in the previous article.
That is, it would work like this using Template Literals.
<div className={`${styles.card} ${styles.cardActive}`}>
And like this with an array
<div className={[styles.card, styles.cardActive].join(' ')}>
Global Exceptions
Sometimes, even within a module, we need to define a global class. For example, to override a style from a third-party library that inserts HTML outside of our control.
To “escape” the local scope, we use the :global pseudo-class.
/* MyComponent.module.css */
.local {
color: red; /* Will be transformed into ._local_hash */
}
:global(.external-library) {
color: blue; /* Will remain as is: .external-library */
}
