React CSS Modules

CSS Modules solve one of the most frustrating problems in larger React applications: class name conflicts. When two components use the same class name in different CSS files, their styles can accidentally overwrite each other. CSS Modules prevent this by automatically making every class name unique to the file it comes from.

The Problem CSS Modules Solve

Diagram: Global CSS name collision

Header.css          Footer.css
.container {        .container {
  padding: 20px;      padding: 0;
  background: blue;   background: black;
}                   }

Browser renders BOTH rules for .container
The second rule wins, overriding Header's container style!
This is a global namespace collision.

In a large app with dozens of files, tracking down which CSS file is responsible for an unexpected style is a debugging nightmare. CSS Modules eliminate the problem at the source.

How CSS Modules Work

Name your CSS file with .module.css at the end. React (via Webpack or Vite) processes it and transforms every class name into a unique identifier at build time.

/* Card.module.css */
.container {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
}

.title {
  font-size: 20px;
  font-weight: bold;
  color: #222;
}

.body {
  font-size: 14px;
  color: #555;
}
/* Card.jsx */
import styles from "./Card.module.css";

function Card({ title, body }) {
  return (
    <div className={styles.container}>
      <h3 className={styles.title}>{title}</h3>
      <p className={styles.body}>{body}</p>
    </div>
  );
}

What the browser actually sees

/* Transformed class names in the browser */
.Card_container_x7k2q { ... }
.Card_title_3mn8p { ... }
.Card_body_9qr1s { ... }

/* Your code  */   /* What browser gets */
styles.container → "Card_container_x7k2q"
styles.title     → "Card_title_3mn8p"
styles.body      → "Card_body_9qr1s"

Another component's .container gets a completely different unique name. No collision possible.

Applying Multiple Classes

Use template literals to combine multiple CSS Module class names.

/* Button.module.css */
.btn {
  padding: 10px 24px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

.primary {
  background-color: #007bff;
  color: white;
}

.secondary {
  background-color: #6c757d;
  color: white;
}

.large {
  padding: 14px 32px;
  font-size: 16px;
}
/* Button.jsx */
import styles from "./Button.module.css";

function Button({ label, variant = "primary", size }) {
  const classNames = [
    styles.btn,
    styles[variant],
    size === "large" ? styles.large : "",
  ].filter(Boolean).join(" ");

  return <button className={classNames}>{label}</button>;
}

// Usage
<Button label="Save" variant="primary" />
<Button label="Cancel" variant="secondary" />
<Button label="Submit" variant="primary" size="large" />

Composing Styles with composes

CSS Modules support a special composes keyword that lets one class inherit styles from another — similar to extending a class.

/* styles.module.css */
.base {
  padding: 10px 16px;
  border-radius: 4px;
  cursor: pointer;
}

.success {
  composes: base;
  background-color: #28a745;
  color: white;
}

.danger {
  composes: base;
  background-color: #dc3545;
  color: white;
}
function Buttons() {
  return (
    <div>
      <button className={styles.success}>Confirm</button>
      <button className={styles.danger}>Delete</button>
    </div>
  );
}

Global Overrides Inside a CSS Module

Sometimes you need to style a third-party component whose class names are fixed. Use the :global selector to write a style that is not scoped.

/* App.module.css */
.wrapper :global(.external-library-btn) {
  border-radius: 0; /* Override third-party button style */
}

Use :global sparingly. It reintroduces the global namespace problem you are trying to avoid.

CSS Modules with Vite and Create React App

Both Vite and Create React App support CSS Modules out of the box — no extra installation needed. The only requirement is naming the file with .module.css.

/* Works automatically */
Card.module.css       ← CSS Module
Card.css              ← Regular global CSS (no scoping)
Card.module.scss      ← CSS Module with SCSS (needs sass package)

Comparison: CSS Modules vs Other Approaches

Approach         Scope           Dynamic styles   Setup needed
--------------------------------------------------------------
Global CSS       Global          Limited          None
CSS Modules      Component       Via classes      None (built-in)
Styled Comp.     Component       Full prop-based  Install package
Tailwind         Utility-based   Via classes      Config needed
Inline styles    Component       Full             None

Common CSS Modules Patterns

Pattern: Status-based styling

/* Badge.module.css */
.badge { padding: 4px 8px; border-radius: 12px; font-size: 12px; }
.active { background: #d4edda; color: #155724; }
.inactive { background: #f8d7da; color: #721c24; }
.pending { background: #fff3cd; color: #856404; }
function Badge({ status }) {
  return (
    <span className={`${styles.badge} ${styles[status]}`}>
      {status}
    </span>
  );
}

<Badge status="active" />
<Badge status="pending" />

Summary

CSS Modules automatically scope class names to the file they are defined in, preventing naming conflicts across components. Name CSS files with .module.css and import them as objects. Access class names through the imported object — styles.className. Combine multiple classes with template literals or the filter-join pattern. Use composes for class inheritance within the same module. CSS Modules work without any extra installation in modern React setups like Vite and Create React App.

Leave a Comment

Your email address will not be published. Required fields are marked *