|$ curl https://forge-ai.dev/api/markdown?path=docs/css/css-modules
$cat docs/css-modules.md
updated This Week·11 min read·published

CSS Modules

CSSModulesBuild ToolsIntermediate
What are 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}
Local Scoping by Default

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.

local-scoping.css
CSS
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

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.

React

Card.jsx
JSX
1import styles from './Card.module.css';
2
3export 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)

Card.vue
VUE
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>
preview
CSS Modules vs Global CSS
AspectGlobal CSSCSS Modules
NamespaceGlobal — all styles share one scopeLocal — scoped per file
Name collisionsFrequent — require BEM or conventionsImpossible — auto-generated unique names
Dead code eliminationManual — hard to track usageAutomatic — unused imports show clearly
Build stepNone — direct link in HTMLRequired — webpack/vite/Next.js
Global stylesNatural — everything is globalExplicit — use :global() selector
ThemingSimple — CSS custom properties cascadeSame — works with custom properties
global-vs-module.css
CSS
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}
CSS Modules with TypeScript

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
2declare module '*.module.css' {
3 const classes: { readonly [key: string]: string };
4 export default classes;
5}
6
7// Or more specific — typed module declarations
8declare module '*.module.css' {
9 const classes: { readonly [key: string]: string };
10 export default classes;
11}
12
13// In a component — fully typed
14import styles from './Button.module.css';
15
16const className: string = styles.button; // OK
17styles['button--primary']; // OK
18styles.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
2npm install --save-dev typed-css-modules
3
4# Generate .d.ts for all CSS modules
5tcm 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
2module.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

vite.config.js
JavaScript
1// vite.config.js
2export 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

webpack.config.js
JavaScript
1// webpack.config.js
2module.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};
Composition Patterns

CSS Modules support several patterns for composing styles together, both within and across modules.

Multiple Classes

multiple-classes.jsx
CSS
1/* In JavaScript — multiple classes */
2<div className={`${styles.card} ${styles.rounded} ${styles.shadow}`} />
3
4/* With classnames utility */
5import classNames from 'classnames';
6<div className={classNames(styles.card, styles.rounded, styles.shadow)} />

Conditional Classes

conditional-classes.jsx
JSX
1function 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

merging-classes.jsx
JSX
1function 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>
Best Practices
Keep CSS Modules colocated with their component: Button/Button.module.css
Use camelCase class names for easier JS access: styles.buttonPrimary
Use :global() sparingly — only for truly global styles like resets and animations
Combine CSS Modules with CSS custom properties for theming
Avoid composes across modules — prefer the classnames utility instead
Name modules *.module.css consistently to avoid confusion with global files
Use CSS Modules + BEM naming for the best DX: scoping + readable output
$Blueprint — Engineering Documentation·Section ID: CSS-33·Revision: 1.0