Local Scoping: How CSS Modules Actually Work
Solving CSS's Global Namespace Problem
In plain CSS, all class names share one global namespace — two unrelated files both defining .button or .container can accidentally collide and override each other. CSS Modules solve this by having a build step transform class names into unique, locally-scoped identifiers per file.
The Transform-and-Export Pattern
/* Button.module.css */
.button {
padding: 8px 16px;
}import styles from './Button.module.css'
function Button() {
return <button className={styles.button}>Click</button>
// renders class="Button_button__a1b2c"
}The .module.css naming convention signals the build tool (Webpack, Vite) to process a file as a CSS Module — generating a unique hashed name and exporting a mapping (a plain JS object) from the original name to the generated one. Component code imports that object and references styles.button rather than a hardcoded string.
Conditional Classes & the Typo Gotcha
Since the mapping is a plain object, conditional class logic uses ordinary JS: styles[isActive ? 'active' : 'inactive']. A real gotcha: referencing a misspelled or non-existent key (styles.typoedClassName) returns undefined silently rather than a build error — no class applied, no crash — which is why some teams adopt typed CSS Modules tooling to catch such typos.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free