CSS Modules
css modules css modules scope class names at build time — import styles from './button.module.css' yields hashed selectors like button_primary_x7f2a
Introduction
CSS Modules scope class names at build time — import styles from './Button.module.css' yields hashed selectors like Button_primary_x7f2a so styles never leak globally. Staff engineers choose CSS Modules when teams want real CSS syntax with component isolation without runtime CSS-in-JS cost.
CSS Modules are the pragmatic middle ground between global BEM and runtime styled-components — zero JS bytes for styling on the client.
Business problem
Business pressure: React apps with global CSS import order bugs — third-party widget loads after app CSS and wins cascade. CSS Modules localize failure to one component import.
- Isolation: Hash suffix prevents accidental global collisions.
- Performance: No runtime style injection — static CSS file cacheable at CDN.
- Migration: Incremental adoption — one component at a time.
Why this feature exists
Platform history: Glen Maddern and colleagues formalized CSS Modules circa 2015 as webpack css-loader feature — addressing React ecosystem global CSS pain.
- Problem solved: Global namespace pollution in component frameworks.
- Rejected alternative: Inline styles only — no pseudo-classes, media queries, or keyframes cleanly.
- Modern role: Vite, Next.js App Router, CRA — default or optional CSS Modules support.
Browser rendering perspective
Rendering impact: Identical to plain CSS after build — hashed class strings are normal class selectors. No JS execution required for styles to apply.
- SSR: Next.js extracts critical CSS; hashed classes in SSR HTML match client bundle.
Internal browser workflow
Workflow: Author .module.css → bundler scopes selectors → JS imports map local names → apply className={styles.root}.
Feature deep dive
Local scope by default — :global(.legacy) escapes for third-party integration.
/* Button.module.css */.root { padding: 0.5rem 1rem; border-radius: 6px; }.primary { background: var(--primary); color: #fff; }/* Button.tsx */import styles from './Button.module.css';export function Button({ primary }) {return <button className={primary ? styles.primary : styles.root} />;}
Syntax
Composition: composes: root from './base.module.css' — merge classes at build time. :global for unscoped rules inside module file.
Examples
Global escape for body scroll lock:
/* Modal.module.css */:global(body.modal-open) { overflow: hidden; }.backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); }
Real-world use
Create React App default styling. Many Next.js apps use CSS Modules for leaf components while Tailwind handles layout. GitHub uses CSS Modules in parts of github.com React surfaces.
Real production example
Pattern: TypeScript typings via typed-css-modules; stylelint on **/*.module.css; visual regression per component story.
Enterprise use case
Enterprise: Hybrid stacks — DS React components with CSS Modules + token CSS variables on :root from design tokens package.
Accessibility considerations
A11y: Modules do not affect semantics — use correct elements. Global escape for focus traps must not break other modals.
Performance considerations
Performance: Static CSS — optimal for cache. Split chunks per route when using dynamic import of modules.
SEO considerations
SEO: SSR with CSS Modules outputs normal class attributes — no SEO penalty vs global CSS.
Scalability considerations
Scale: composes chains across packages need stable build graph — version token package independently.
Common production issues
Failures: Overusing :global recreates global cascade. Dynamic class access styles[dynamic] breaks if purge misconfigured.
Debugging guide
Debug: DevTools shows hashed classes — map to source via Sources panel original file. React DevTools shows className prop.
Best practices
- Keep :global rare and documented.
- Share tokens via CSS variables on :root, not duplicated values.
- One module per component file.
- Use TypeScript definitions for class keys.
Anti-patterns
- Importing module styles into global index without tree-shaking plan.
- String concatenation class names bypassing type safety.
Trade-offs
- Benefit: isolation, static CSS, familiar syntax.
- Cost: no dynamic theming without variables; composes complexity.
- vs CSS-in-JS: Modules win runtime perf; CSS-in-JS wins prop-driven dynamic styles.
Architecture review questions
- When is :global approved in our codebase?
- How do tokens reach module files?
Interview questions
How do CSS Modules achieve scoping?(Intermediate)
Build tool hashes class names and transforms selectors in the module file to match — local names in JS import map to hashed strings. No runtime scope injection.
Follow-up: How do you style a third-party child component?
Hands-on exercise
Exercise: Convert global BEM button to CSS Module. Prove no global leakage with second conflicting .button rule.
Staff engineer notes
- CSS Modules + design tokens is the enterprise sweet spot for React without runtime CSS cost.
Common pitfalls
- :global creep — every file escapes to global.
Try it yourself
Edit the CSS panel — the preview updates live. Use DevTools Performance and Accessibility panels to validate.
Try it yourself
Summary
CSS Modules scope styles via build-time class hashing — delivering component isolation with standard CSS syntax and CDN-cacheable stylesheets.
Key takeaways
- Build-time scoped class hashes isolate component CSS.
- Static CSS — no runtime styling JS cost.