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.cssout of the box. - Next.js and Create React App enable them by default.
- Astro supports them directly in components.
- webpack needs the
modulesoption, 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
composesfor shared declarations instead of duplicating rules. - Reach for
:globalonly for resets, body classes and third-party markup. - Theme with CSS custom properties so styles can change without a rebuild.
- Use a
cxhelper orclsxfor 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.csssuffix and getting global styles instead. - Trying to use a class name directly in a string instead of via the imported map.
- Overusing
:globaland 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.