Skip to main content
  • Created:
  • Updated:
  • Author:
    Takeshi Takatsudo

CSS Modules Strategy

The Problem

CSS operates in a global namespace by default. Every class name you write is available everywhere, which means any stylesheet can accidentally overwrite another's styles. In a growing project, naming collisions become inevitable — two developers independently create a .title class, or a new feature's .container conflicts with an existing one. Dead CSS accumulates because no one can safely remove a class without searching the entire codebase to confirm it is unused.

For AI agents, global CSS is particularly hazardous. An agent generating a .card or .header class has no way to know whether those names already exist in the project. The result is unintended style overrides that are difficult to diagnose.

The Solution

CSS Modules solve the global namespace problem at build time. Each CSS file is treated as a local scope — class names are automatically transformed into unique identifiers (typically by appending a hash) so they cannot collide with classes from other files. You import styles as a JavaScript object and reference them by key:

import styles from './Button.module.css';

function Button() {
return <button className={styles.primary}>Click me</button>;
}

The build tool (Webpack, Vite, etc.) transforms .primary into something like .Button_primary_x7f2a, ensuring uniqueness without any naming convention. The mapping between your authored name and the generated name is handled automatically through the imported styles object.

Code Examples

How Class Name Scoping Works

When you write a CSS Modules file, the build tool transforms each class name into a unique, hashed version. The original names you write are for readability — the browser sees only the generated names.

Before and After: Class Name Transformation

Basic Usage: Import and Apply

In a CSS Modules workflow, you import the CSS file as a JavaScript module. The imported object maps your authored class names to the generated unique names.

// Button.module.css
// .primary { background: #3b82f6; color: #fff; }
// .secondary { background: #e2e8f0; color: #1e293b; }

import styles from './Button.module.css';

function Button({ variant = 'primary', children }) {
return (
<button className={styles[variant]}>
{children}
</button>
);
}
CSS Modules: Component with Style Variants

No Collisions Across Components

The key benefit of CSS Modules is that the same class name in different files produces different generated names. Two components can both use .title without any conflict.

Same Class Name, Different Components — No Collision

Composition with composes

CSS Modules support a composes keyword that lets you combine classes from the same file or from other files. This is the CSS Modules way to share common styles without duplication.

/* shared.module.css */
.baseButton {
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}

/* Button.module.css */
.primary {
composes: baseButton from './shared.module.css';
background: #3b82f6;
color: #fff;
}
CSS Modules: composes for Style Reuse

Global Selectors with :global

Sometimes you need to target a class name that is not locally scoped — for example, a third-party library class or a body-level state class. CSS Modules provides :global() for this purpose.

/* Locally scoped by default */
.container {
padding: 16px;
}

/* Opt out of scoping for specific selectors */
:global(.ReactModal__Overlay) {
background: rgba(0, 0, 0, 0.5);
}

/* Mix local and global */
.container :global(.highlight) {
background: yellow;
}
CSS Modules: Local vs Global Selectors

Comparison with Other Approaches

ApproachScoping MechanismNamingBuild Tool RequiredRuntime Cost
CSS ModulesBuild-time hashAuthor-chosen, locally scopedYesNone
BEMManual naming conventionManual discipline (block__element--modifier)NoNone
Utility-firstNo custom classes neededPredefined utility vocabularyRecommendedNone
CSS-in-JSRuntime or build-timeCo-located with JS componentsDependsOften runtime
  • vs BEM: BEM achieves collision avoidance through manual naming discipline. CSS Modules automate it — you write .title and the tool guarantees uniqueness. BEM is convention, CSS Modules are enforcement.
  • vs Utility-first: Utility frameworks eliminate custom class names entirely. CSS Modules still let you write semantic class names but scope them automatically. The two can coexist — some projects use CSS Modules for component-specific styles and utilities for layout.
  • vs CSS-in-JS: Libraries like styled-components scope styles at runtime by injecting <style> tags. CSS Modules achieve the same scoping at build time with zero runtime cost. CSS-in-JS offers dynamic styling based on props; CSS Modules require CSS custom properties or conditional class names for the same.

Common Mistakes

Using :global too liberally

Wrapping everything in :global defeats the purpose of CSS Modules. Reserve it for targeting third-party classes or body-level states — not for convenience.

/* Wrong: bypasses scoping for no reason */
:global(.card) {
padding: 16px;
}
:global(.card-title) {
font-size: 18px;
}

/* Correct: use local scoping by default */
.card {
padding: 16px;
}
.cardTitle {
font-size: 18px;
}

Not leveraging composes

Duplicating shared styles across multiple .module.css files when composes exists for exactly this purpose.

/* Wrong: duplicated base styles */
/* Button.module.css */
.primary {
border: none;
padding: 10px 20px;
border-radius: 6px;
background: #3b82f6;
color: #fff;
}
.secondary {
border: none;
padding: 10px 20px;
border-radius: 6px;
background: #e2e8f0;
color: #1e293b;
}

/* Correct: compose shared base */
.primary {
composes: base from './shared.module.css';
background: #3b82f6;
color: #fff;
}
.secondary {
composes: base from './shared.module.css';
background: #e2e8f0;
color: #1e293b;
}

Mixing global and module styles in one component

Importing both a global stylesheet and a CSS Module for the same component creates ambiguity about which styles are scoped and which are not.

// Wrong: mixing scoping models
import './global-card.css'; // global styles
import styles from './Card.module.css'; // scoped styles

// Correct: use one approach consistently per component
import styles from './Card.module.css';

Using kebab-case class names in JavaScript

CSS Modules export class names as object properties. Kebab-case names require bracket notation, which is less ergonomic.

// Awkward: bracket notation needed
<div className={styles['card-title']}>

// Better: use camelCase in .module.css
// .cardTitle { font-size: 18px; }
<div className={styles.cardTitle}>

When to Use

CSS Modules work well for:

  • React, Vue, and other framework projects with a build tool (Webpack, Vite) already in place
  • Component libraries where style isolation per component is critical
  • Projects migrating from global CSS that want scoping without adopting CSS-in-JS or a utility framework
  • Teams that prefer writing standard CSS but need automated collision prevention

CSS Modules are less suitable when:

  • No build tool is available — CSS Modules require a bundler to transform class names
  • You want utility-first styling — Tailwind or UnoCSS removes the need for custom class names entirely
  • You need highly dynamic styles — CSS-in-JS may be more ergonomic when styles depend on many component props

References