CSS Architecture

CSS Modules

CSS Modules give you plain, hand-written CSS with class names scoped to a single component, so styles never leak or collide across a large codebase.

intermediate12 min readUpdated Sep 15, 2026
Button.module.css
css
/* Button.module.css */
.button {
  border-radius: 0.5rem;
  padding: 0.5rem 1rem;
  background: #2563eb;
  color: #fff;
}
.button:hover { background: #3b82f6; }
File suffix
.module.css
Scoping
Build-time hash
Output
Plain static CSS
Runtime cost
None
Composition
composes
Works with
Any modern bundler

Why it matters

Why CSS Modules matter

No more collisions

Class names are hashed per file, so .title in one component can never clash with .title in another.

Zero runtime

The compiler rewrites class names at build time. The browser gets ordinary CSS with no JavaScript overhead.

Real CSS, scoped

You keep writing normal CSS — variables, media queries, pseudo-classes — with no new syntax to learn.

The big picture

The three ideas behind CSS Modules

Scoping, composition and zero runtime — that is the whole model, and it is why CSS Modules stay simple at scale.

Local scope

Isolation

Every class in a module file is local by default and gets a unique, generated name.

Composition

Reuse

The composes keyword shares declarations between classes without duplicating rules.

The build step

Compilation

The bundler transforms class names and hands your component a map from local name to generated name.

CSS Modules at a glance

The essentials

The .module.css file

Any stylesheet ending in .module.css is treated as a scoped module.

Importing styles

Import the file and reference generated class names through the returned object.

composes

Build a class from one or more others, in the same file or across files.

:global

Escape the local scope when you really need a global class or a third-party selector.

Variables

CSS custom properties and preprocessor variables still work exactly as before.

Conditional classes

Combine the styles map with a helper to toggle classes based on props or state.

A short history

A quiet fix for the global CSS problem

  1. 2015

    Introduced at scale

    CSS Modules emerge from the React community as a way to scope CSS without new syntax.

    15
  2. 2017

    Bundler support

    webpack, and later Vite and Parcel, make .module.css a first-class convention.

    17
  3. 2020

    Mainstream default

    Frameworks such as Next.js and Create React App enable CSS Modules out of the box.

    20
  4. Today

    A dependable baseline

    A zero-runtime option that pairs well with any component framework or plain HTML build.

    Today

The complete guide

CSS Modules: Everything you need to know

What are CSS Modules?

CSS Modules solve a problem every growing codebase eventually hits: global class names. In plain CSS, a .title class in one file and a .title class in another are the same class. Whoever loads last wins, and nobody notices until a component looks wrong in a place they never touched.

A CSS Module is a stylesheet where every class is local by default. You write normal CSS in a file named Button.module.css, import it into your component, and the build step rewrites the class names to be unique. The browser still receives plain, static CSS — there is no runtime and no new syntax.

How scoping works

The transformation happens at build time. Given this module:

/* Card.module.css */
.card {
  border: 1px solid #e5e7eb;
  border-radius: 0.75rem;
}

.title {
  font-size: 1.25rem;
  font-weight: 600;
}

The bundler generates something like .Card_card__a1b2c and .Card_title__d4e5f, and gives your component an object that maps the names you wrote to the generated ones.

// Card.jsx
import styles from "./Card.module.css";

export function Card({ title, children }) {
  return (
    <div className={styles.card}>
      <h3 className={styles.title}>{title}</h3>
      {children}
    </div>
  );
}

You keep using readable class names in source. The uniqueness is an implementation detail the tooling handles for you.

Naming and file layout

The convention is one module per component, sitting next to it.

components/
├── Card.jsx
├── Card.module.css
├── Button.jsx
└── Button.module.css

Only files ending in .module.css are treated as scoped modules, so you can mix global stylesheets and modules in the same project. Because each module is independent, the same class name can appear in dozens of files without conflict.

Composition with composes

composes lets a class inherit the declarations of another class. It is a build-time feature — the browser sees a single element with two generated classes.

/* Button.module.css */
.base {
  display: inline-flex;
  align-items: center;
  border-radius: 0.5rem;
  padding: 0.5rem 1rem;
}

.primary {
  composes: base;
  background: #2563eb;
  color: #ffffff;
}

.large {
  composes: base;
  padding: 0.75rem 1.5rem;
  font-size: 1.125rem;
}

You can compose from another module too, which is how shared design primitives spread across a codebase:

/* Alert.module.css */
.alert {
  composes: card from "./Card.module.css";
  border-left: 4px solid #f59e0b;
}

Composition keeps rules in one place and lets the cascade do the combining, instead of repeating declarations.

Escaping to global

Sometimes you need to target something outside the component: a body class, a third-party widget, or a utility from a global stylesheet. :global opts a selector out of scoping.

/* Modal.module.css */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
}

:global(body.no-scroll) {
  overflow: hidden;
}

You can also keep a selector local while making part of it global, which is handy for wrapper classes around library markup. Use this sparingly; the value of CSS Modules is that almost everything stays local.

Variables and theming

CSS custom properties work exactly as they do in any stylesheet, and they are the natural way to theme a module-based app.

/* theme.css */
:root {
  --color-primary: #2563eb;
  --radius: 0.5rem;
}

[data-theme="dark"] {
  --color-primary: #60a5fa;
}
/* Button.module.css */
.button {
  border-radius: var(--radius);
  background: var(--color-primary);
  color: #ffffff;
}

Because custom properties cascade, changing a theme at the root updates every module without touching a single component file. Sass variables also work, but they are resolved at build time and cannot change at runtime.

Conditional and combined classes

Components often need to toggle classes based on props or state. The styles object is just a map, so combine it with a small helper.

// Button.jsx
import styles from "./Button.module.css";

function cx(...classes) {
  return classes.filter(Boolean).join(" ");
}

export function Button({ variant = "primary", disabled }) {
  return (
    <button
      className={cx(styles.base, styles[variant], disabled && styles.disabled)}
      disabled={disabled}
    >
      Save
    </button>
  );
}

Libraries like clsx or classnames do the same job with a few more features. The pattern is always: look up the generated class by its local name and join the ones you need.

Framework setup

Most modern tools support CSS Modules with no configuration.

  • Vite processes *.module.css out of the box.
  • Next.js and Create React App enable them by default.
  • Astro supports them directly in components.
  • webpack needs the modules option, usually via a loader rule.

The only real requirement is that your bundler understands the .module.css convention. If it does, you are ready to write scoped CSS today.

When to use CSS Modules

CSS Modules shine when you want to keep writing real CSS but avoid global collisions — design systems, component libraries and large applications with many contributors. They are a great fit when your team already knows CSS well and does not want a new abstraction.

They are less compelling if you prefer utilities in the markup (see Tailwind) or if you need styles computed from JavaScript props at runtime (see styled-components). For a straightforward, zero-runtime way to scope styles, though, they are hard to beat.

Best practices

  • Keep one module per component and colocate the files.
  • Use composes for shared declarations instead of duplicating rules.
  • Reach for :global only for resets, body classes and third-party markup.
  • Theme with CSS custom properties so styles can change without a rebuild.
  • Use a cx helper or clsx for conditional classes.
  • Name classes by role (primary, disabled), not by appearance.
  • Keep specificity low; modules remove the need for deep selectors.

Common mistakes

  • Forgetting the .module.css suffix and getting global styles instead.
  • Trying to use a class name directly in a string instead of via the imported map.
  • Overusing :global and reintroducing the collisions you were avoiding.
  • Deeply nesting selectors and fighting the generated class names.
  • Duplicating declarations instead of composing them.
  • Assuming class names stay the same between builds and hard-coding them.

Where to go next

CSS Modules are one point on the styling spectrum. Compare them with Tailwind CSS for utility-first styling and styled-components for CSS-in-JS, then brush up on the CSS fundamentals that all three rely on.

Scoping a class

The generated name is unique per module, so .title cannot collide with another component's .title.

CSS Module
/* Card.module.css */
.title {
  font-size: 1.25rem;
  font-weight: 600;
}
Global CSS
/* styles.css */
.title {
  font-size: 1.25rem;
}
/* another file redefines .title */

Reusing styles

composes shares declarations without duplicating them or adding a new class to the markup.

Prefer
.base {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
}
.primary {
  composes: base;
  background: #2563eb;
}
Avoid
.primary {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background: #2563eb;
}

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning CSS Modules?

Our interactive tutorial walks you through CSS Modules step by step — with quizzes and real code you can run in the browser.