Skip to main content

CSS Modules in React: Local Scoped Styles (Part 1)

CSS Modules transform your stylesheets at build time to automatically scope class names locally to the component that imports them. By renaming .css to .module.css and importing styles as a JavaScript object, you eliminate class-name conflicts and can safely reuse names like .error or .title across multiple components without collisions.

Key Takeaways

  • CSS Modules solve the global scope problem: every class name is scoped locally to its component by default
  • Rename files from .css to .module.css and import as an object: import styles from './Button.module.css'
  • Access classes via dot notation: className={styles.error} renders as className="Button_error__aX7yz" (unique hashed name)
  • No naming conflicts: two components can both define .error without overriding each other

The Global Scope Problem CSS Modules Solve

In standard CSS, class names are global. If two files define the same class, the second one overwrites the first—causing unpredictable, hard-to-debug styling issues.

Example of the problem:

Two developers independently create components with .error classes:

Button.css

.error {
background-color: red;
color: white;
}

Card.css

.error {
border: 1px solid red;
color: red;
}

When both files are imported into your app, whichever loads last wins. The first developer's red background style vanishes, replaced by the second developer's border-only style. This is a classic CSS name collision.

CSS Modules prevent this entirely. By scoping each class locally to its component, you can reuse .error, .title, .wrapper, or any simple name in every stylesheet without conflict.

How CSS Modules Work in React

The .module.css Naming Convention

To enable CSS Modules, rename your stylesheet from .css to .module.css. That's all it takes in modern React toolchains (Vite, Create React App, Next.js).

Create two files in your component folder:

  • Button.jsx
  • Button.module.css

Writing Module Styles

The CSS syntax is unchanged—just plain CSS:

/* Button.module.css */
.error {
background-color: red;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
}

.success {
background-color: green;
color: white;
}

Importing and Using Scoped Styles

Import the module as an object (conventionally named styles). The object keys are your CSS class names, and the values are unique, hashed class names generated at build time:

// Button.jsx
import React from 'react';
import styles from './Button.module.css';

function Button({ variant = 'success' }) {
return (
<button className={styles[variant]}>
Click Me
</button>
);
}

export default Button;

How this works:

  1. Import syntax: import styles from './Button.module.css' imports the module as a JavaScript object.
  2. Dot notation: styles.error accesses the .error class.
  3. Bracket notation: styles['my-class'] accesses kebab-case class names.
  4. Dynamic values: You can use variables or state to dynamically choose which class to apply.

Understanding the Build-Time Transformation

When you run npm run build (or the dev server processes your code), the CSS Modules processor does two things:

  1. Generates unique class names using a hash. Your .error class becomes something like Button_error__aX7yz to guarantee global uniqueness.
  2. Creates the styles object that maps your original names to the hashed ones:
// Imported styles object looks like:
{
error: 'Button_error__aX7yz',
success: 'Button_success__k2Lmn'
}

So when you write <button className={styles.error}>, React renders <button class="Button_error__aX7yz">. The unique class name ensures zero collisions, even if another component in the same app defines .error.

Multiple Classes and Conditional Styling

You can combine multiple CSS Module classes using string concatenation or libraries:

import styles from './Input.module.css';
import { useState } from 'react';

function Input() {
const [hasError, setHasError] = useState(false);

return (
<input
className={`${styles.input} ${hasError ? styles.withError : ''}`}
onBlur={() => setHasError(true)}
/>
);
}

export default Input;

Alternatively, use a utility library like clsx for cleaner code:

npm install clsx
import clsx from 'clsx';
import styles from './Input.module.css';

function Input({ hasError }) {
return (
<input
className={clsx(styles.input, hasError && styles.withError)}
/>
);
}

export default Input;

Best Practices

Use CSS Modules for component styles. This is the default choice. It provides local scope with full CSS power (media queries, pseudo-selectors, animations).

Use plain .css only for global styles like index.css (resets, typography, CSS variables) that you intentionally want site-wide.

Prefer camelCase class names in your CSS (myClass not my-class) so you can use dot notation (styles.myClass) rather than bracket notation.

Name files after their component: Button.module.css for Button.jsx, Header.module.css for Header.jsx. This makes it obvious which styles belong to which component.

Frequently Asked Questions

Can I use CSS Modules with Tailwind CSS?

Yes, but they serve different purposes. CSS Modules provide component-scoped styles; Tailwind provides utility classes. Many projects use both: Tailwind for layout/spacing, CSS Modules for component-specific styling. However, most projects pick one approach to avoid duplication.

What happens if I use a class name that doesn't exist in my CSS file?

You'll get undefined in your className. For example, styles.nonExistent will be undefined, and React will silently ignore it. Use TypeScript or a linter to catch these errors at development time.

Can I use CSS Modules with CSS-in-JS libraries like Emotion or Styled Components?

CSS Modules and CSS-in-JS libraries solve the same scoping problem in different ways. Most projects choose one. CSS Modules use actual CSS files; CSS-in-JS embeds styles in JavaScript. Both eliminate naming conflicts.

Do CSS Modules work with pseudo-selectors and media queries?

Yes, 100%. CSS Modules support all standard CSS features:

/* Button.module.css */
.button:hover {
opacity: 0.8;
}

.button:active {
transform: scale(0.98);
}

@media (max-width: 600px) {
.button {
padding: 8px 12px;
}
}

How do I use CSS variables (custom properties) in CSS Modules?

CSS variables work normally:

/* Button.module.css */
:root {
--primary-color: #007bff;
}

.button {
background-color: var(--primary-color);
}

Or define them in a global index.css and use them in your modules.

Further Reading