Introduction
One of the most frustrating problems in large CSS codebases is name collisions. Two developers independently create a .card class, and one silently overrides the other. CSS Modules solve this by automatically scoping class names to the component that imports them. Every class name becomes unique at build time, eliminating collisions entirely while letting you write plain, familiar CSS. No runtime overhead, no new syntax to learn — just CSS that works predictably at scale.
Key Concepts
- Scoped class names: CSS Module class names are transformed at build time into unique identifiers (e.g.,
.cardbecomes.Card_card_x7k3a), preventing collisions across components. .module.cssextension: Files must use the.module.css(or.module.scss) extension to be treated as CSS Modules by the bundler.- Import as object: You import the CSS Module as a JavaScript object where keys are the original class names and values are the generated unique names.
- Composition: CSS Modules support a
composeskeyword that lets classes inherit styles from other classes, similar to Sass@extendbut with explicit dependency tracking.
Real World Context
Consider a design system used across 50 micro-frontends. Without CSS Modules, every team must coordinate class names to avoid collisions, or use BEM naming conventions that produce long, unwieldy names like .design-system__card--highlighted. With CSS Modules, every team writes simple, short class names like .card and .highlighted, and the build tool guarantees uniqueness. This simplifies the CSS, reduces bugs, and eliminates cross-team coordination for naming.
Deep Dive
Create a CSS Module file:
css/* Button.module.css */ .container { display: flex; gap: 8px; padding: 16px; } .button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: background-color 0.2s; } .primary { composes: button; background-color: #2563eb; color: white; } .primary:hover { background-color: #1d4ed8; } .secondary { composes: button; background-color: #e5e7eb; color: #1f2937; } .secondary:hover { background-color: #d1d5db; }
Import and use it in React:
tsximport styles from "./Button.module.css"; function ButtonGroup() { return ( <div className={styles.container}> <button className={styles.primary}>Save</button> <button className={styles.secondary}>Cancel</button> </div> ); }
Combining multiple classes:
tsximport styles from "./Card.module.css"; function Card({ highlighted, className }: { highlighted?: boolean; className?: string }) { const classes = [ styles.card, highlighted && styles.highlighted, className, ].filter(Boolean).join(" "); return <div className={classes}>Content</div>; }
At build time, the class names are transformed:
html<!-- What you write --> <div class="container"> <button class="primary button">Save</button> </div> <!-- What the browser sees --> <div class="Button_container_a3x2k"> <button class="Button_primary_b7y9z Button_button_c1m4n">Save</button> </div>
Common Pitfalls
- Forgetting the
.module.cssextension — A regular.cssfile is treated as global CSS. If you import it as an object, the keys will be undefined. Always use.module.cssfor scoped styles. - Using hyphens in class names — CSS class names with hyphens (like
.my-class) require bracket notation:styles["my-class"]. Prefer camelCase (.myClass) to use dot notation:styles.myClass.
Best Practices
- Use camelCase class names — Write
.cardTitleinstead of.card-titleso you can access it asstyles.cardTitleinstead ofstyles["card-title"]. - Colocate CSS Modules with components — Place
Button.module.cssnext toButton.tsxin the same directory. This makes the style-component relationship obvious and keeps related code together.
Summary
- CSS Modules automatically scope class names to prevent collisions, using the
.module.cssfile extension. - Import CSS Modules as JavaScript objects where keys are class names and values are generated unique identifiers.
- Use
composesfor style inheritance and camelCase class names for convenient dot notation access.
Code Examples
// Card.module.css (colocated with component)
// .card { border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
// .title { font-size: 18px; font-weight: bold; margin-bottom: 8px; }
// .highlighted { border: 2px solid #2563eb; }
import styles from "./Card.module.css";
function Card({ title, highlighted, children }: {
title: string;
highlighted?: boolean;
children: React.ReactNode;
}) {
return (
<div className={`${styles.card} ${highlighted ? styles.highlighted : ""}`}>
<h3 className={styles.title}>{title}</h3>
{children}
</div>
);
}