CSS Modules Strategy
The Problem
CSS operates in a global namespace by default. Every class name you write is available everywhere, which means any stylesheet can accidentally overwrite another's styles. In a growing project, naming collisions become inevitable — two developers independently create a .title class, or a new feature's .container conflicts with an existing one. Dead CSS accumulates because no one can safely remove a class without searching the entire codebase to confirm it is unused.
For AI agents, global CSS is particularly hazardous. An agent generating a .card or .header class has no way to know whether those names already exist in the project. The result is unintended style overrides that are difficult to diagnose.
The Solution
CSS Modules solve the global namespace problem at build time. Each CSS file is treated as a local scope — class names are automatically transformed into unique identifiers (typically by appending a hash) so they cannot collide with classes from other files. You import styles as a JavaScript object and reference them by key:
import styles from './Button.module.css';
function Button() {
return <button className={styles.primary}>Click me</button>;
}
The build tool (Webpack, Vite, etc.) transforms .primary into something like .Button_primary_x7f2a, ensuring uniqueness without any naming convention. The mapping between your authored name and the generated name is handled automatically through the imported styles object.
Code Examples
How Class Name Scoping Works
When you write a CSS Modules file, the build tool transforms each class name into a unique, hashed version. The original names you write are for readability — the browser sees only the generated names.
Basic Usage: Import and Apply
In a CSS Modules workflow, you import the CSS file as a JavaScript module. The imported object maps your authored class names to the generated unique names.
// Button.module.css
// .primary { background: #3b82f6; color: #fff; }
// .secondary { background: #e2e8f0; color: #1e293b; }
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return (
<button className={styles[variant]}>
{children}
</button>
);
}
No Collisions Across Components
The key benefit of CSS Modules is that the same class name in different files produces different generated names. Two components can both use .title without any conflict.
Composition with composes
CSS Modules support a composes keyword that lets you combine classes from the same file or from other files. This is the CSS Modules way to share common styles without duplication.
/* shared.module.css */
.baseButton {
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
/* Button.module.css */
.primary {
composes: baseButton from './shared.module.css';
background: #3b82f6;
color: #fff;
}
Global Selectors with :global
Sometimes you need to target a class name that is not locally scoped — for example, a third-party library class or a body-level state class. CSS Modules provides :global() for this purpose.
/* Locally scoped by default */
.container {
padding: 16px;
}
/* Opt out of scoping for specific selectors */
:global(.ReactModal__Overlay) {
background: rgba(0, 0, 0, 0.5);
}
/* Mix local and global */
.container :global(.highlight) {
background: yellow;
}
Comparison with Other Approaches
| Approach | Scoping Mechanism | Naming | Build Tool Required | Runtime Cost |
|---|---|---|---|---|
| CSS Modules | Build-time hash | Author-chosen, locally scoped | Yes | None |
| BEM | Manual naming convention | Manual discipline (block__element--modifier) | No | None |
| Utility-first | No custom classes needed | Predefined utility vocabulary | Recommended | None |
| CSS-in-JS | Runtime or build-time | Co-located with JS components | Depends | Often runtime |
- vs BEM: BEM achieves collision avoidance through manual naming discipline. CSS Modules automate it — you write
.titleand the tool guarantees uniqueness. BEM is convention, CSS Modules are enforcement. - vs Utility-first: Utility frameworks eliminate custom class names entirely. CSS Modules still let you write semantic class names but scope them automatically. The two can coexist — some projects use CSS Modules for component-specific styles and utilities for layout.
- vs CSS-in-JS: Libraries like styled-components scope styles at runtime by injecting
<style>tags. CSS Modules achieve the same scoping at build time with zero runtime cost. CSS-in-JS offers dynamic styling based on props; CSS Modules require CSS custom properties or conditional class names for the same.
Common Mistakes
Using :global too liberally
Wrapping everything in :global defeats the purpose of CSS Modules. Reserve it for targeting third-party classes or body-level states — not for convenience.
/* Wrong: bypasses scoping for no reason */
:global(.card) {
padding: 16px;
}
:global(.card-title) {
font-size: 18px;
}
/* Correct: use local scoping by default */
.card {
padding: 16px;
}
.cardTitle {
font-size: 18px;
}
Not leveraging composes
Duplicating shared styles across multiple .module.css files when composes exists for exactly this purpose.
/* Wrong: duplicated base styles */
/* Button.module.css */
.primary {
border: none;
padding: 10px 20px;
border-radius: 6px;
background: #3b82f6;
color: #fff;
}
.secondary {
border: none;
padding: 10px 20px;
border-radius: 6px;
background: #e2e8f0;
color: #1e293b;
}
/* Correct: compose shared base */
.primary {
composes: base from './shared.module.css';
background: #3b82f6;
color: #fff;
}
.secondary {
composes: base from './shared.module.css';
background: #e2e8f0;
color: #1e293b;
}
Mixing global and module styles in one component
Importing both a global stylesheet and a CSS Module for the same component creates ambiguity about which styles are scoped and which are not.
// Wrong: mixing scoping models
import './global-card.css'; // global styles
import styles from './Card.module.css'; // scoped styles
// Correct: use one approach consistently per component
import styles from './Card.module.css';
Using kebab-case class names in JavaScript
CSS Modules export class names as object properties. Kebab-case names require bracket notation, which is less ergonomic.
// Awkward: bracket notation needed
<div className={styles['card-title']}>
// Better: use camelCase in .module.css
// .cardTitle { font-size: 18px; }
<div className={styles.cardTitle}>
When to Use
CSS Modules work well for:
- React, Vue, and other framework projects with a build tool (Webpack, Vite) already in place
- Component libraries where style isolation per component is critical
- Projects migrating from global CSS that want scoping without adopting CSS-in-JS or a utility framework
- Teams that prefer writing standard CSS but need automated collision prevention
CSS Modules are less suitable when:
- No build tool is available — CSS Modules require a bundler to transform class names
- You want utility-first styling — Tailwind or UnoCSS removes the need for custom class names entirely
- You need highly dynamic styles — CSS-in-JS may be more ergonomic when styles depend on many component props