CSS Modules are CSS files where all class names and animation names are scoped locally by default. When you import a CSS Module into a component, the build tool generates unique class names that prevent collisions with styles in other modules.
Unlike traditional CSS where every selector lives in the global namespace, CSS Modules give you component-level scoping — the same class name in two different files produces two different generated classes at build time. This eliminates class name conflicts without requiring naming conventions.
The core feature of CSS Modules is automatic local scoping. Every class name is transformed into a unique identifier that only applies within the importing component.
The hashed class names are deterministic — they stay consistent between builds and only change when the source CSS changes. This enables long-term caching while guaranteeing no collisions.
📝
note
Only class names and animation names are scoped. Element selectors (h1, p, div) and global selectors remain global. CSS Modules encourages class-based styling.
Composing Classes
CSS Modules support a composes keyword that lets you inherit styles from another class within the same module or from another module entirely.
composing.css
CSS
1
/* typography.module.css */
2
.heading {
3
font-family: 'JetBrains Mono', monospace;
4
font-weight: 600;
5
letter-spacing: -0.02em;
6
}
7
8
.error {
9
color: #EF4444;
10
}
11
12
/* button.module.css */
13
.base {
14
composes: heading from './typography.module.css';
15
padding: 8px 16px;
16
border-radius: 4px;
17
border: none;
18
cursor: pointer;
19
}
20
21
.danger {
22
composes: base;
23
composes: error;
24
background: #0D0D0D;
25
}
⚠
warning
composes only works on class selectors within the same file or an imported module. It cannot compose from external CSS libraries or @imported files.
Using with React and Vue
CSS Modules integrate natively with the most popular frameworks. The pattern is consistent: import the module and use the imported object's properties as class names.
TypeScript needs type declarations for CSS Module imports. Without them, importing a .module.css file will cause a type error.
css-modules.d.ts
TypeScript
1
// src/globals.d.ts
2
declare module '*.module.css' {
3
const classes: { readonly[key: string]: string };
4
export default classes;
5
}
6
7
// Or more specific — typed module declarations
8
declare module '*.module.css' {
9
const classes: { readonly[key: string]: string };
10
export default classes;
11
}
12
13
// In a component — fully typed
14
import styles from './Button.module.css';
15
16
const className: string = styles.button; // OK
17
styles['button--primary'];// OK
18
styles.nonExistent;// string (not ideal)
19
20
// For strict typing, use typed-css-modules or similar tools
21
// This generates .d.ts files alongside your CSS modules
For stricter typing, tools like typed-css-modules or ts-plugin-css-modules generate .d.ts files that only allow valid class names:
typed-css-modules.sh
Bash
1
# Install type generation
2
npm install --save-dev typed-css-modules
3
4
# Generate .d.ts for all CSS modules
5
tcm src --pattern "**/*.module.css"
6
7
# This produces:
8
# Button.module.css.d.ts
9
# Contains: export const button: string;
10
# export const 'button--primary': string;
Build Tool Configuration
CSS Modules are supported out of the box in most modern frameworks, but understanding the configuration gives you control over naming patterns and global exceptions.
Next.js
Next.js supports CSS Modules natively. Any file named *.module.css is automatically treated as a CSS Module.
next.config.js
JavaScript
1
// next.config.js — customization is rarely needed