LEARN · DEBUGGING GUIDE

CSS Modules Class Not Found: Why Your Imported Styles Object Returns Undefined

If you're importing a CSS Module but the class name is undefined in your JSX, the problem is almost never the CSS itself. It's a build pipeline issue—wrong loader, broken naming convention, or missing TypeScript declaration.

IntermediateBuild tools8 min read

What this usually means

CSS Modules work by locally scoping class names—the build tool rewrites class selectors to unique hashes and exports a mapping object. When you import styles from './Button.module.css' and get undefined for a known class, it means the build pipeline did not process that file as a CSS Module. The most common causes are: the file isn't named with the .module.css extension (or the convention your bundler expects), the CSS loader configuration doesn't enable modules for that file pattern, or the module resolution is being overridden by another loader (like sass-loader or postcss-loader). In TypeScript projects, you also need a declaration file to tell TypeScript that importing .module.css returns an object, not a string.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check the filename: does it end with .module.css? Next.js, CRA, and webpack's css-loader require that pattern.
  • 2Open your browser's devtools and inspect the element. Look at the class attribute—is it the raw hash or the literal string 'undefined'?
  • 3console.log(styles) right after the import. If you see {} or { default: ... }, your loader isn't treating the import as a CSS Module.
  • 4In your terminal, run the build with verbose logging (e.g., webpack --stats verbose) and look for the module rule that processes .module.css files.
  • 5For TypeScript: check if you have a src/globals.d.ts or similar with a declare module '*.module.css' declaration. Without it, TS won't know the shape of the import.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchwebpack.config.js or next.config.js — specifically the css-loader options in the rule for /\.module\.css$/
  • searchpackage.json — verify you have css-loader (if using webpack directly) at version 5+ (modules option changed in v5)
  • searchtsconfig.json — check that 'moduleResolution' is 'node' or 'bundler' and that you include a .d.ts file for CSS modules
  • searchThe actual .module.css file itself — ensure the class name you're trying to use actually exists and isn't commented out
  • searchBrowser devtools → Network tab → filter by .css — confirm the CSS file was loaded and contains the hashed class name
  • searchYour bundler's logs — look for warnings like 'Module not found: Error: Can't resolve' or 'No module rule matches'
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningFile not following the .module.css naming convention (e.g., styles.css instead of styles.module.css)
  • warningcss-loader configuration missing modules: { localIdentName: ... } or modules: true in the rule for .module.css files
  • warningMultiple CSS loaders in the chain—the first loader might consume the import before css-loader can process it as a module
  • warningTypeScript missing a declare module '*.module.css' — the import resolves to an empty object because TS can't find the declaration
  • warningWhen using Sass, the sass-loader compiles .scss to CSS but the rule doesn't include .module.scss (it only matches .module.css)
  • warningA hot-reload or caching issue where the build didn't recompile the CSS module after a rename—clear your cache and restart
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildRename the file to Component.module.css (or .module.scss) if it doesn't follow the convention
  • buildUpdate webpack configuration: in the rule for /\.module\.css$/, ensure css-loader has options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]' } }
  • buildFor TypeScript: create a src/types/css.d.ts file with: declare module '*.module.css' { const classes: { [key: string]: string }; export default classes; }
  • buildIf using Next.js and the issue persists, check your next.config.js for any css-loader customization that might override the default CSS Modules behavior
  • buildClear your bundler cache: rm -rf .next (Next.js) or rm -rf node_modules/.cache (webpack) and restart the dev server
  • buildEnsure the import path is correct—relative imports must start with ./ or ../, not an absolute path without a marker
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedconsole.log(styles) should output an object like { button: 'Button_button_1a2b3' } — not undefined or { default: ... }
  • verifiedInspect the DOM element with devtools — the class attribute should be the hashed class name, not 'undefined'
  • verifiedRun the production build (npm run build) and check that the CSS file includes the hashed selector and the JS bundle imports the mapping object
  • verifiedFor TypeScript: after adding the declaration file, the import should no longer show a red squiggly line
  • verifiedAdd a unique class like .test-class { color: red; } and verify it appears in the computed styles in the browser
  • verifiedRun a diff on the compiled CSS file between a working and non-working build to see if the module was processed
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't add a global CSS file import that matches the same rule as the module—use separate rules for .module.css and .css files
  • warningDon't use both CSS Modules and regular CSS imports in the same file unless you have separate rules—it can cause conflicts
  • warningDon't ignore TypeScript errors by using // @ts-ignore — you're hiding the real problem
  • warningDon't rename .module.css files without clearing the bundler cache—old hashes linger and cause confusion
  • warningDon't assume that because one module works, all will—check if the failing file has a different extension or path
  • warningDon't use the CSS Modules import as a side-effect import (import './styles.module.css') — you must assign it to a variable
( 07 )War story

The Production Disappearing Button Styles

Frontend EngineerReact 18, Next.js 12, TypeScript 4.6

Timeline

  1. 09:15Deploy to production. User reports that the primary CTA button is invisible.
  2. 09:22I check the component: <Button className={styles.primary}> — styles.primary is undefined.
  3. 09:30Console.log(styles) in dev shows { primary: 'Button_primary_abc12' }. In production, it's {}.
  4. 09:45I inspect the network tab: the CSS file is loaded, but the class .Button_primary_abc12 doesn't exist in it.
  5. 10:00Comparison with last working build reveals a missing .module.css rule in the production webpack config.
  6. 10:10Fix: add the rule for /\.module\.css$/ to the prod config. Rebuild and redeploy.
  7. 10:15Button now visible. Root cause: a code merge removed the module rule from the production webpack config.

The morning started with a Slack alert: 'The sign-up button is missing on the landing page.' I opened the page and saw a blank space where the bright green button should be. The component code looked fine—it was importing styles from './Button.module.css' and using styles.primary. But when I inspected the DOM, the class attribute was literally 'undefined'. The button had no styles at all.

In development, everything worked perfectly. I ran the production build locally and saw that the CSS file was indeed being served—it contained all the global styles—but the hashed class names from the module were missing. That's when I realized the production webpack config didn't have a rule for .module.css files. A recent refactor had cleaned up the config and accidentally removed the module rule.

I re-added the rule with css-loader's modules option, rebuilt, and redeployed. The button was back. The lesson: always keep development and production configs in sync, and validate by comparing the compiled CSS output between environments. A simple diff would have caught this in code review.

Root cause

The production webpack configuration was missing the module rule for .module.css files, so css-loader never processed them as CSS Modules. The import returned an empty object.

The fix

Added a rule to the production webpack config: { test: /\.module\.css$/, use: ['style-loader', { loader: 'css-loader', options: { modules: true } }] }.

The lesson

Always keep development and production build configurations in sync. Use a shared config or validate with a diff. Also, add a test that checks the imported styles object is not empty.

( 08 )How CSS Modules Work Under the Hood

CSS Modules are a build-time feature. When you write import styles from './Button.module.css', the bundler (webpack, Vite, or Next.js) reads the CSS file, scopes each class name by appending a unique hash, and exports a JavaScript object that maps the original class name to the hashed version. For example, .primary becomes .Button_primary_abc12, and styles.primary equals 'Button_primary_abc12'.

The key is that the bundler must be configured to treat .module.css files differently from regular .css files. In webpack, this is done with a rule that matches the file pattern and applies css-loader with modules: true. If the rule is missing or misconfigured, the import falls back to global CSS behavior—the import resolves to an empty object or the CSS is injected as a side effect with no export.

( 09 )The TypeScript Declaration File Gotcha

TypeScript doesn't know what import styles from './Button.module.css' means out of the box. It expects a module declaration for '*.module.css'. Without it, TypeScript will either error (if noImplicitAny is off, it may infer any) or the import resolves to an empty object type. This is a common source of 'class not found' errors that only surface at runtime.

The fix is to create a declaration file (e.g., src/types/css.d.ts) with: declare module '*.module.css' { const classes: { [key: string]: string }; export default classes; }. This tells TypeScript that importing a .module.css file returns an object with string keys and string values. Make sure this file is included in your tsconfig.json's include array.

( 10 )Naming Conventions Across Build Tools

Different tools have different default conventions. Create React App and Next.js require the .module.css extension. Vite uses .module.css by default but can be configured. If you're using a custom webpack setup, you might have chosen a different pattern like .scoped.css. The rule in the webpack config must match exactly.

A common mistake is to have a file named styles.css when it should be styles.module.css. The rule for .module.css files won't match, and the import will be treated as a global CSS import. Always check the extension first. If you need both global and module CSS, use separate file naming and separate rules.

( 11 )Debugging with Bundler Logs and Cache

When the class is undefined, the fastest way to diagnose is to enable verbose bundler logs. For webpack, add --stats verbose to your build command. Look for the line that processes your .module.css file. You should see something like 'Module: ./src/Button.module.css' with the css-loader and style-loader applied. If you see only the file path without loaders, the rule didn't match.

Another trick: run the build with the --json flag to get a compilation stats JSON. Search for 'moduleCss' or the file name. If the module doesn't appear, your import might be broken or the rule is missing. Also, clear your cache regularly—rm -rf node_modules/.cache or .next. Stale cache can cause old module exports to persist.

Frequently asked questions

Why does my CSS Module work in development but not in production?

This usually means your production webpack configuration is different from development. The most common difference is missing or misconfigured css-loader rules for .module.css files. Since development often uses a more lenient config (like in Create React App's react-scripts), the issue only surfaces in production. Compare your production webpack config to development, especially the module.rules section.

I have a .module.scss file. Do I need a different rule?

Yes, if you're using Sass, you need a rule that matches .module.scss (or .module.sass) and applies both sass-loader and css-loader with modules enabled. Many configurations only match .module.css, so your .module.scss file falls through to the regular CSS rule. Add a separate rule for /\.module\.(scss|sass)$/ with the appropriate loaders.

Why does console.log(styles) show { default: { primary: 'hash' } }?

This happens when you use an older version of css-loader (v4 or earlier) or when you import the CSS module using a default import syntax that isn't recognized. In css-loader v5+, the default export is the classes object directly. If you see a nested default property, you might have a misconfigured modules option or you're using a transpiler that wraps the import. Upgrade css-loader to v5+ and ensure modules: true is set.

Can I use both CSS Modules and regular CSS imports in the same project?

Yes, but you must have separate webpack rules for .module.css and .css files. The rule for .module.css should apply css-loader with modules: true, while the rule for regular .css should apply css-loader without modules (or with modules: false). If the same rule processes both, the module files won't be scoped correctly.

I'm using Next.js and the class is undefined. What's the most common fix?

Next.js supports CSS Modules out of the box for files ending with .module.css. If it's not working, first check the file extension. If it's correct, try deleting the .next folder and restarting the dev server. If that doesn't work, check your next.config.js for any css-loader customizations that might override the default behavior. Also ensure you're using the import correctly: import styles from './Component.module.css' and then className={styles.myClass}.