Full-project lesson · needs Node.js, npm and real project files, so it does not run in the in-page playground.
CSS Modules keep plain CSS but make every class name local to the file that imports it. Two components can both use a class called .title and never clash. Vite supports CSS Modules with no setup: any file named *.module.css is a module.
How it works
Create src/components/Alert.module.css:
.alert {
padding: 12px 16px;
border-radius: 8px;
background: #fff3cd;
border: 1px solid #ffe69c;
}
.error {
background: #f8d7da;
border-color: #f1aeb5;
}
Import it as an object in src/components/Alert.jsx. Each class in the file becomes a property of that object:
import styles from './Alert.module.css'
function Alert({ type = 'warning', children }) {
const className = type === 'error' ? `${styles.alert} ${styles.error}` : styles.alert
return (
<div role="alert" className={className}>
{children}
</div>
)
}
export default Alert
And in src/App.jsx:
import Alert from './components/Alert.jsx'
function App() {
return (
<div>
<Alert>Check your details.</Alert>
<Alert type="error">Payment failed.</Alert>
</div>
)
}
export default App
(Built with Vite and checked in a browser: the error alert gets the red background from .error.)
What the browser sees
styles.alert is not the string "alert". Vite rewrites the class name to a unique, hashed one (something like _alert_1x2yz_1), both in the CSS and in the styles object. The hashes mean .alert here can never collide with .alert anywhere else. If you inspect the page you will see those generated names. That is expected.
Naming and syntax rules
- Use camelCase class names (
.errorMessage) so you can writestyles.errorMessage. A kebab-case name needsstyles['error-message']. - Use classes only in module files. Element selectors (
h1 {}) and IDs stay global, and mixing them is confusing. - A selector like
.card .titlemakes both local.
Combining classes
Template strings work; so does a small helper that joins truthy names. The library clsx does this, but a few lines are enough:
function cx(...names) {
return names.filter(Boolean).join(' ')
}
<div className={cx(styles.alert, isError && styles.error)} />
See Conditional Classes.
Global exceptions
To opt out of scoping for one selector, wrap it in :global:
.card :global(.external-widget) { margin: 0; }
Use it sparingly, for third-party class names you do not control.
Reusing styles: composes
CSS Modules can inherit another class from the same or another module:
.base { padding: 8px; }
.primary { composes: base; background: #1a5276; color: #fff; }
Setting className={styles.primary} then also applies base.
Pros and cons
| Good | Be aware |
|---|---|
| Real CSS, with no new syntax to learn | One extra import per component |
| No name collisions | Class names in the browser are generated, which makes them harder to target from outside (including in tests) |
| Works with the whole of CSS (media queries, pseudo-classes, variables) | Dynamic values still need inline styles or CSS variables |
| Build tools remove unused modules | Need a bundler that supports it (Vite, webpack and others do) |
Common mistakes
- Naming the file
Alert.cssinstead ofAlert.module.css. Then it is a global file, andstylesis empty. - Using a class that does not exist in the file.
styles.missingisundefined, so no class is applied and no error appears. - Kebab-case names with dot notation:
styles.error-messageis a subtraction.
Key points
- Name the file
*.module.css, import it as an object and usestyles.className. - Class names are hashed, so they are unique to that file.
- Combine classes with template strings or a tiny helper; use
:globalrarely.