Skip to content

React Tutorial Styling React Applications

CSS Modules in React: Scoped Styles Without Name Clashes

Lesson 83 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Full-project lesson · needs Node.js, npm and real project files, so it does not run in the in-page playground.

CSS Modules keep plain CSS but make every class name local to the file that imports it. Two components can both use a class called .title and never clash. Vite supports CSS Modules with no setup: any file named *.module.css is a module.

How it works

Create src/components/Alert.module.css:

src/components/Alert.module.css
CSS
.alert {
  padding: 12px 16px;
  border-radius: 8px;
  background: #fff3cd;
  border: 1px solid #ffe69c;
}

.error {
  background: #f8d7da;
  border-color: #f1aeb5;
}

Import it as an object in src/components/Alert.jsx. Each class in the file becomes a property of that object:

src/components/Alert.jsx
JSX
import styles from './Alert.module.css'

function Alert({ type = 'warning', children }) {
  const className = type === 'error' ? `${styles.alert} ${styles.error}` : styles.alert
  return (
    <div role="alert" className={className}>
      {children}
    </div>
  )
}

export default Alert

And in src/App.jsx:

src/App.jsx
JSX
import Alert from './components/Alert.jsx'

function App() {
  return (
    <div>
      <Alert>Check your details.</Alert>
      <Alert type="error">Payment failed.</Alert>
    </div>
  )
}

export default App

(Built with Vite and checked in a browser: the error alert gets the red background from .error.)

What the browser sees

styles.alert is not the string "alert". Vite rewrites the class name to a unique, hashed one (something like _alert_1x2yz_1), both in the CSS and in the styles object. The hashes mean .alert here can never collide with .alert anywhere else. If you inspect the page you will see those generated names. That is expected.

Naming and syntax rules

  • Use camelCase class names (.errorMessage) so you can write styles.errorMessage. A kebab-case name needs styles['error-message'].
  • Use classes only in module files. Element selectors (h1 {}) and IDs stay global, and mixing them is confusing.
  • A selector like .card .title makes both local.

Combining classes

Template strings work; so does a small helper that joins truthy names. The library clsx does this, but a few lines are enough:

script.js
JS
function cx(...names) {
  return names.filter(Boolean).join(' ')
}

<div className={cx(styles.alert, isError && styles.error)} />

See Conditional Classes.

Global exceptions

To opt out of scoping for one selector, wrap it in :global:

script.php
PHP
.card :global(.external-widget) { margin: 0; }

Use it sparingly, for third-party class names you do not control.

Reusing styles: composes

CSS Modules can inherit another class from the same or another module:

style.css
CSS
.base { padding: 8px; }
.primary { composes: base; background: #1a5276; color: #fff; }

Setting className={styles.primary} then also applies base.

Pros and cons

GoodBe aware
Real CSS, with no new syntax to learnOne extra import per component
No name collisionsClass names in the browser are generated, which makes them harder to target from outside (including in tests)
Works with the whole of CSS (media queries, pseudo-classes, variables)Dynamic values still need inline styles or CSS variables
Build tools remove unused modulesNeed a bundler that supports it (Vite, webpack and others do)

Common mistakes

  • Naming the file Alert.css instead of Alert.module.css. Then it is a global file, and styles is empty.
  • Using a class that does not exist in the file. styles.missing is undefined, so no class is applied and no error appears.
  • Kebab-case names with dot notation: styles.error-message is a subtraction.

Key points

  • Name the file *.module.css, import it as an object and use styles.className.
  • Class names are hashed, so they are unique to that file.
  • Combine classes with template strings or a tiny helper; use :global rarely.