CSS Modules
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.
1/* Button.module.css */2.button {3 background: #00FF41;4 color: #0D0D0D;5 border: none;6 padding: 8px 16px;7 border-radius: 4px;8 font-family: 'JetBrains Mono', monospace;9 cursor: pointer;10}11 12.button--primary {13 background: #FFB000;14}15 16.button:hover {17 filter: brightness(1.1);18}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.
| 1 | /* styles.module.css */ |
| 2 | .header { background: #0D0D0D; } |
| 3 | .title { color: #E0E0E0; font-size: 1.5rem; } |
| 4 | .text { color: #808080; line-height: 1.6; } |
| 5 | |
| 6 | /* Generated output (webpack/vite) */ |
| 7 | .styles_header__abc123 { background: #0D0D0D; } |
| 8 | .styles_title__def456 { color: #E0E0E0; font-size: 1.5rem; } |
| 9 | .styles_text__ghi789 { color: #808080; line-height: 1.6; } |
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
CSS Modules support a composes keyword that lets you inherit styles from another class within the same module or from another module entirely.
| 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
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.
React
| 1 | import styles from './Card.module.css'; |
| 2 | |
| 3 | export function Card({ title, children, variant }) { |
| 4 | return ( |
| 5 | <div className={classNames( |
| 6 | styles.card, |
| 7 | variant === 'featured' && styles['card--featured'] |
| 8 | )}> |
| 9 | <h3 className={styles.card__title}>{title}</h3> |
| 10 | <div className={styles.card__body}>{children}</div> |
| 11 | </div> |
| 12 | ); |
| 13 | } |
Vue (SFC)
| 1 | <template> |
| 2 | <div :class="$style.card"> |
| 3 | <h3 :class="$style.card__title">{{ title }}</h3> |
| 4 | <div :class="$style.card__body"> |
| 5 | <slot /> |
| 6 | </div> |
| 7 | </div> |
| 8 | </template> |
| 9 | |
| 10 | <style module> |
| 11 | .card { |
| 12 | background: #1A1A2E; |
| 13 | border-radius: 8px; |
| 14 | padding: 16px; |
| 15 | } |
| 16 | |
| 17 | .card__title { |
| 18 | color: #E0E0E0; |
| 19 | font-size: 1.25rem; |
| 20 | } |
| 21 | |
| 22 | .card__body { |
| 23 | color: #808080; |
| 24 | font-size: 0.875rem; |
| 25 | } |
| 26 | </style> |
| Aspect | Global CSS | CSS Modules |
|---|---|---|
| Namespace | Global — all styles share one scope | Local — scoped per file |
| Name collisions | Frequent — require BEM or conventions | Impossible — auto-generated unique names |
| Dead code elimination | Manual — hard to track usage | Automatic — unused imports show clearly |
| Build step | None — direct link in HTML | Required — webpack/vite/Next.js |
| Global styles | Natural — everything is global | Explicit — use :global() selector |
| Theming | Simple — CSS custom properties cascade | Same — works with custom properties |
| 1 | .card { |
| 2 | /* This is local — hashed by CSS Modules */ |
| 3 | padding: 16px; |
| 4 | } |
| 5 | |
| 6 | :global(.theme-dark) { |
| 7 | /* This stays global — available everywhere */ |
| 8 | --bg: #0D0D0D; |
| 9 | --text: #E0E0E0; |
| 10 | } |
| 11 | |
| 12 | :global(.sr-only) { |
| 13 | /* Global utility — screen reader only */ |
| 14 | position: absolute; |
| 15 | width: 1px; |
| 16 | height: 1px; |
| 17 | overflow: hidden; |
| 18 | clip: rect(0, 0, 0, 0); |
| 19 | } |
TypeScript needs type declarations for CSS Module imports. Without them, importing a .module.css file will cause a type error.
| 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:
| 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; |
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.
| 1 | // next.config.js — customization is rarely needed |
| 2 | module.exports = { |
| 3 | // CSS Modules work out of the box |
| 4 | // Customize generated class names: |
| 5 | cssModules: { |
| 6 | localIdentName: '[name]__[local]--[hash:base64:5]', |
| 7 | }, |
| 8 | }; |
Vite
| 1 | // vite.config.js |
| 2 | export default { |
| 3 | css: { |
| 4 | modules: { |
| 5 | localsConvention: 'camelCaseOnly', // styles.myClass |
| 6 | generateScopedName: '[name]__[local]___[hash:base64:5]', |
| 7 | scopeBehaviour: 'local', // default |
| 8 | globalModulePaths: [/global/, /shared/], |
| 9 | }, |
| 10 | }, |
| 11 | }; |
Webpack
| 1 | // webpack.config.js |
| 2 | module.exports = { |
| 3 | module: { |
| 4 | rules: [ |
| 5 | { |
| 6 | test: /\.module\.css$/, |
| 7 | use: [ |
| 8 | 'style-loader', |
| 9 | { |
| 10 | loader: 'css-loader', |
| 11 | options: { |
| 12 | modules: { |
| 13 | localIdentName: '[hash:base64:8]', |
| 14 | exportLocalsConvention: 'camelCase', |
| 15 | }, |
| 16 | }, |
| 17 | }, |
| 18 | ], |
| 19 | }, |
| 20 | // Regular CSS files — not modules |
| 21 | { |
| 22 | test: /\.css$/, |
| 23 | exclude: /\.module\.css$/, |
| 24 | use: ['style-loader', 'css-loader'], |
| 25 | }, |
| 26 | ], |
| 27 | }, |
| 28 | }; |
CSS Modules support several patterns for composing styles together, both within and across modules.
Multiple Classes
| 1 | /* In JavaScript — multiple classes */ |
| 2 | <div className={`${styles.card} ${styles.rounded} ${styles.shadow}`} /> |
| 3 | |
| 4 | /* With classnames utility */ |
| 5 | import classNames from 'classnames'; |
| 6 | <div className={classNames(styles.card, styles.rounded, styles.shadow)} /> |
Conditional Classes
| 1 | function Badge({ count, variant }) { |
| 2 | return ( |
| 3 | <span className={classNames( |
| 4 | styles.badge, |
| 5 | styles[`badge--${variant}`], |
| 6 | count === 0 && styles['badge--empty'], |
| 7 | )}> |
| 8 | {count} |
| 9 | </span> |
| 10 | ); |
| 11 | } |
Merging with External Classes
| 1 | function Button({ className, children }) { |
| 2 | // Merge external className with module styles |
| 3 | return ( |
| 4 | <button className={classNames(styles.button, className)}> |
| 5 | {children} |
| 6 | </button> |
| 7 | ); |
| 8 | } |
| 9 | |
| 10 | // Usage — consumer adds utility or layout classes |
| 11 | <Button className="mt-4">Submit</Button> |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.