CSS Modules
Learn how CSS Modules automatically scope class names to the component that imports them, eliminating global class name collisions.
Introduction
In the last lesson you saw that plain CSS files share one global namespace — meaning two components can accidentally define the same class name and silently override each other's styles. CSS Modules solve this exact problem by automatically scoping every class name to the specific component that imports it.
This lesson goes deep on CSS Modules: how the file naming convention works, how imported class names map onto generated, unique names, and how to combine multiple classes together conditionally, a very common need once you start building interactive UI.
- What CSS Modules are and the problem they solve.
- How a file named Button.module.css becomes automatically scoped.
- How to import and use scoped class names through the styles object.
- How to combine multiple scoped classes together, including conditionally.
What are CSS Modules?
A CSS Module is simply a regular CSS file with one special naming convention: it ends in .module.css instead of just .css. Most modern React build tools (Vite, Create React App, Next.js) recognize this suffix automatically and process the file differently than an ordinary stylesheet.
Instead of injecting the class names exactly as written, the build tool rewrites every class name in the file into something guaranteed to be unique across your entire app — often by appending a hash, like .card_a8f3e. Because the generated name is unique, it can never collide with a class of the same name defined in another component's module file.
Automatic Scoping
Every class in a .module.css file is scoped to the component that imports it.
No Collisions
Two components can both use .card without ever conflicting.
Ordinary CSS Syntax
You write completely normal CSS — no new syntax to learn.
Build-Tool Powered
The scoping happens automatically at build time; no manual setup is required in modern toolchains.
Creating a Module File
To create a CSS Module for a Button component, name the file Button.module.css and place it alongside Button.jsx. Inside it, write ordinary CSS rules exactly as you always would.
.button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
}
.primary {
background-color: #4f46e5;
color: white;
}
.secondary {
background-color: #e5e7eb;
color: #111827;
}Nothing here looks different from a normal CSS file. The scoping magic happens entirely when this file is imported into a component.
Using Scoped Class Names
When you import a .module.css file, you do not get individual class name strings — you get a single styles object whose keys match your class names and whose values are the generated, scoped names. You then apply them using styles.className in the className attribute.
import styles from './Button.module.css';
function Button({ children }) {
return (
<button className={styles.button}>
{children}
</button>
);
}
export default Button;<button class="Button_button__a8f3e">Click Me</button>You can name the import anything — styles, classes, css — but styles is the conventional name most codebases use. Each property on it, like styles.button, resolves to a unique generated class name string.
How Scoping Actually Works
Behind the scenes, the build tool parses your .module.css file, renames every selector to something unique — typically combining the file name, the original class name, and a short hash — and generates a mapping object. That mapping object is exactly what gets imported as styles in your component.
Button.module.css is loaded
↓
Every class selector is renamed uniquely
↓
(.button becomes .Button_button__a8f3e)
↓
A JS mapping object is generated
↓
{ button: "Button_button__a8f3e", primary: "Button_primary__c91d2" }
↓
That object is what you import as "styles"Because two different components each get their own unique hash, a .button class in Button.module.css and a completely separate .button class in Card.module.css will never collide, even though you wrote the same class name in both files.
Combining Classes Conditionally
Real components often need more than one class at once — a base style plus a variant, or a base style plus a conditional state like "active" or "disabled." Since styles.button and styles.primary are just plain strings, you can combine them using a template literal or simple string concatenation.
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
const variantClass = variant === 'primary' ? styles.primary : styles.secondary;
return (
<button className={`${styles.button} ${variantClass}`}>
{children}
</button>
);
}
export default Button;For more complex conditional class logic — say, toggling an "active" class based on a boolean prop — many teams use a small utility library like classnames or clsx to keep the logic readable.
import clsx from 'clsx';
import styles from './Button.module.css';
function Button({ isActive, children }) {
return (
<button className={clsx(styles.button, { [styles.primary]: isActive })}>
{children}
</button>
);
}
export default Button;Example: A Styled Alert Component
Here is a small Alert component that uses a base class alongside a conditional variant class, applying everything learned so far.
.alert {
padding: 12px 16px;
border-radius: 6px;
font-size: 0.95rem;
}
.success {
background-color: #dcfce7;
color: #166534;
}
.error {
background-color: #fee2e2;
color: #991b1b;
}import styles from './Alert.module.css';
function Alert({ type = 'success', message }) {
const variantClass = type === 'success' ? styles.success : styles.error;
return (
<div className={`${styles.alert} ${variantClass}`}>
{message}
</div>
);
}
export default Alert;<div class="Alert_alert__f7c21 Alert_error__b02e4">Something went wrong</div>Common Mistakes
- Naming the file Button.css instead of Button.module.css — without the .module suffix, the build tool treats it as a regular, globally scoped stylesheet.
- Using a hyphenated class name like .primary-button in CSS and then trying styles.primary-button in JS, which is invalid JavaScript syntax — use styles['primary-button'] or prefer camelCase class names.
- Forgetting that a missing key on the styles object (a typo) returns undefined, not an error, which silently applies no class at all.
- Hardcoding a raw class name string like className="button" alongside CSS Modules, which bypasses scoping entirely.
Best Practices
- Name CSS Module files after their component: Button.module.css for Button.jsx.
- Prefer camelCase class names in your CSS (e.g. .primaryButton) so they map cleanly to styles.primaryButton.
- Keep one module file per component to keep styles co-located and easy to find.
- Use a small utility like clsx once you have more than one or two conditional classes to combine.
- Remember CSS Modules only scope class names — CSS variables and global element selectors still apply globally.
Frequently Asked Questions
Do I need to install anything to use CSS Modules?
Usually no. Modern tools like Vite, Create React App, and Next.js support the .module.css convention out of the box with zero configuration.
Can I use Sass or Less with CSS Modules?
Yes. Files named Button.module.scss or Button.module.less work the same way, as long as your build tool has the corresponding preprocessor set up.
What happens if I forget to import the styles object?
Referencing styles will throw a ReferenceError since the variable simply does not exist. Always double check your import statement at the top of the file.
Can two components share the same CSS Module file?
Yes, though it is less common. If two components import the same .module.css file, they will both receive the same generated class names, since the scoping is based on the file, not the component.
Are CSS Modules the same as Sass modules?
No, they are different things. CSS Modules refer to the scoping mechanism covered here and work with plain CSS or with preprocessors like Sass — they are not exclusive to Sass.
Key Takeaways
- CSS Modules use the .module.css suffix to enable automatic, per-component class name scoping.
- Importing a module file gives you a styles object mapping your class names to unique generated ones.
- Applying a class means writing className={styles.className} instead of a raw string.
- Multiple classes can be combined with a template literal or a helper library like clsx for conditional logic.
- CSS Modules eliminate the global class name collision problem while keeping ordinary CSS syntax.
Summary
CSS Modules give you the best of both worlds: you write completely normal CSS, but every class name is automatically scoped to the component that imports it, removing the risk of accidental global collisions that plain CSS files carry.
Next, you will look at inline styling — passing style objects directly to elements in JSX — and see exactly where it fits compared to the file-based approach you just learned.
- You understand what CSS Modules are and the problem they solve.
- You can create a .module.css file and import its scoped classes.
- You can combine multiple scoped classes, including conditionally.
- You are ready to explore inline styling.