CSS Modules
01 / 02

Local Scoping: How CSS Modules Actually Work

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