| | |

Parcel 10 ๐Ÿ“ฆ CSS Modules โ€” Scoped styles out of the box

โฑ๏ธ Estimated read: 10 min | |

A CSS Module is a CSS file whose class names are scoped to the module that imports it. The file is written like ordinary CSS, but the class names are rewritten at build time into unique identifiers. The importing JavaScript file receives an object that maps the original names to the generated ones. Two components can both define a .button class, and the two rules will not collide because the generated names include a hash of the file path.

Parcel enables CSS Modules by the filename convention: a file ending in .module.css is a module, and a file ending in .css is global. No configuration is needed for the default behavior. The scoping, the hash, and the mapping object are all produced by the built-in CSS transformer .

This chapter covers the .module.css convention, the import and usage pattern, the default generated class name format, the global escape hatch for a rule that must not be scoped, and the configuration options for customizing the naming pattern and enabling modules globally.

Key point: The default class name is _<local>_<hash> in development and <name>_<local>_<hash> in production. The hash is derived from the file path, so two files with the same class name never collide. The .module.css suffix is what triggers the scoping.


Why CSS Modules exist

The global scope problem. Ordinary CSS is global. A .button rule in header.css applies to every element with class="button" on the page, including the button in footer.css that was supposed to be red. The larger the application, the more likely a class name collision becomes, and the harder it is to trace which file is responsible for a style.

The naming convention problem. Teams invent prefixes (.header-button, .footer-button) or use BEM (.button--primary) to avoid collisions. These conventions work, but they depend on every developer following them consistently. A CSS Module enforces the scoping mechanically: the class name is unique because the build rewrites it, not because the developer remembered to prefix it.

The dead code problem. A global stylesheet accumulates rules that are no longer used. Removing a rule requires checking every file that might reference the class. A CSS Module is tied to the file that imports it. When the importing file is removed, the module has no importers, and the bundler can drop it.


a. The .module.css convention

The suffix is the entire configuration for the default behavior. A file named button.module.css is a module. A file named button.css is global.

/* button.module.css */
.button {
  background: blue;
  padding: 8px 16px;
  border-radius: 4px;
}

The file is written like any other CSS. The class names are local by default; they will be scoped when the file is imported.

The generated class name follows the pattern _<local>_<hash> in development and <name>_<local>_<hash> in production, where <name> is the file name without the extension and <hash> is a short hash of the file path. The development format is shorter for readability in the browser’s developer tools; the production format includes the file name for debugging .


b. Importing and using a module

The default import of a module returns an object. The object’s keys are the original class names, and the values are the generated ones.

import styles from './button.module.css'

element.className = styles.button

At runtime, styles.button is something like _button_a1b2c3 in development or button_button_a1b2c3 in production. The CSS file contains the corresponding rule.

The import works from JavaScript and TypeScript. Parcel’s transformer generates the .d.ts type declaration for the module automatically, so TypeScript knows the shape of the styles object without a manual declaration file .

A module can also be imported for its side effects, but the class mapping is only available if the default import is used. A bare import './button.module.css' includes the styles but does not expose the class names, which defeats the purpose of using a module.


c. The global escape hatch and configuration

A rule inside a module can be marked as global with the :global pseudo-class. The rule is not scoped, and its class name is preserved.

/* button.module.css */
:global(.external-widget) {
  margin: 0;
}

This is useful when a module needs to style an element from a third-party library whose class name it does not control. The :global selector applies to the class inside the parentheses, and the rest of the module remains scoped.

The naming pattern and the global mode are configurable through the @parcel/transformer-css key in package.json.

{
  "@parcel/transformer-css": {
    "modules": {
      "pattern": "[name]-[local]-[hash]",
      "global": false
    }
  }
}

The pattern field controls the generated class name format. The [name], [local], and [hash] placeholders correspond to the file name, the original class name, and the content hash. The global field, when set to true, treats every CSS file as a module, not just the ones with the .module.css suffix .


Complete Example Session

/* ============================================
   PART 1: THE MODULE FILE
   ============================================ */
/* button.module.css */
.button {
  background: blue;
  padding: 8px 16px;
}

.primary {
  background: green;
}
// ============================================
// PART 2: THE IMPORT
// ============================================
import styles from './button.module.css'

console.log(styles)
// { button: '_button_a1b2c3', primary: '_primary_d4e5f6' }
// ============================================
// PART 3: THE USAGE
// ============================================
element.className = styles.button
// The element's class is '_button_a1b2c3'
/* ============================================
   PART 4: THE OUTPUT CSS
   ============================================ */
._button_a1b2c3 {
  background: blue;
  padding: 8px 16px;
}

._primary_d4e5f6 {
  background: green;
}
/* ============================================
   PART 5: A SECOND MODULE WITH THE SAME CLASS
   ============================================ */
/* card.module.css */
.button {
  background: red;
}
// ============================================
// PART 6: NO COLLISION
// ============================================
import buttonStyles from './button.module.css'
import cardStyles from './card.module.css'

buttonStyles.button  // '_button_a1b2c3'
cardStyles.button    // '_button_g7h8i9'

// The two classes are different.
// Both rules are applied to their respective elements.
/* ============================================
   PART 7: THE GLOBAL ESCAPE HATCH
   ============================================ */
/* button.module.css */
:global(.external-widget) {
  margin: 0;
}

.button {
  background: blue;
}
/* ============================================
   PART 8: THE GLOBAL OUTPUT
   ============================================ */
.external-widget {
  margin: 0;
}

._button_a1b2c3 {
  background: blue;
}

/* The global selector is not scoped. */
// ============================================
// PART 9: THE CONFIGURATION
// ============================================
{
  "@parcel/transformer-css": {
    "modules": {
      "pattern": "[name]-[local]",
      "global": false
    }
  }
}
// ============================================
// PART 10: THE GLOBAL MODE
// ============================================
{
  "@parcel/transformer-css": {
    "modules": {
      "global": true
    }
  }
}

// Every CSS file is treated as a module.
// The .module.css suffix is no longer required.

The ten parts covered the module file, the import, the usage, the output CSS, a second module with the same class, the collision avoidance, the :global escape hatch, the global output, the configuration, and the global mode.


Quick Reference

The convention

FilenameBehavior
button.module.cssCSS Module, scoped
button.cssGlobal CSS

Import and usage

ItemExample
Importimport styles from './button.module.css'
Class mapstyles.button
Generated name (dev)_button_a1b2c3
Generated name (prod)button_button_a1b2c3

Selectors

SelectorBehavior
.buttonScoped
:global(.external)Not scoped
:local(.button)Explicitly scoped

Configuration

OptionEffect
modules.patternCustom class name format
modules.globalTreat all CSS as modules
[name]File name placeholder
[local]Original class name
[hash]Content hash

Best Practices

โœ… Do This:

/* Use the .module.css suffix */
/* button.module.css */                                     // โœ…
// Use the default import to get the class map
import styles from './button.module.css'                     // โœ…
// Reference the class through the map
element.className = styles.button                            // โœ…
/* Use :global for third-party class names */
:global(.external-widget) { margin: 0; }                    // โœ…

โŒ Don’t Do This:

// Import a module for its side effects only
import './button.module.css'  // styles not available        // โŒ
/* Use a class name without referencing the map */
/* button.module.css */
.button { }  // the DOM needs styles.button                  // โŒ
// Hardcode the generated class name
element.className = '_button_a1b2c3'                         // โŒ

Common Pitfalls

PitfallWhy It HappensFix
styles.button is undefinedFilename missing .moduleRename to button.module.css
Class name not appliedThe class is used directly, not through the mapUse styles.button
Global class not scopedMissing :globalWrap the selector in :global()
TypeScript error on importNo type declarationParcel generates the .d.ts automatically
Collision still occursThe file is a .css, not a .module.cssUse the suffix

Real-World Examples

1. Module file

/* card.module.css */
.card { padding: 16px; }

2. Import and use

import styles from './card.module.css'
element.className = styles.card

3. Two classes

element.className = `${styles.card} ${styles.elevated}`

4. :global selector

:global(.leaflet-container) { z-index: 0; }

5. :local selector

:local(.button) { background: blue; }

6. Custom pattern

{ "modules": { "pattern": "[name]-[local]" } }

7. Global mode

{ "modules": { "global": true } }

8. Multiple classes from one module

import { card, elevated } from './card.module.css'

9. Development class name

_button_a1b2c3

10. Production class name

button_button_a1b2c3

Visual

The scoping mechanism

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  button.module.css                                           โ”‚
โ”‚    .button { background: blue; }                             โ”‚
โ”‚                                                              โ”‚
โ”‚  Build                                                       โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Output CSS                                                  โ”‚
โ”‚    ._button_a1b2c3 { background: blue; }                    โ”‚
โ”‚                                                              โ”‚
โ”‚  Import map                                                  โ”‚
โ”‚    { button: '_button_a1b2c3' }                             โ”‚
โ”‚                                                              โ”‚
โ”‚  The class name is unique to this file.                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

No collision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  button.module.css                                           โ”‚
โ”‚    .button โ†’ _button_a1b2c3                                  โ”‚
โ”‚                                                              โ”‚
โ”‚  card.module.css                                             โ”‚
โ”‚    .button โ†’ _button_d4e5f6                                  โ”‚
โ”‚                                                              โ”‚
โ”‚  The two rules do not override each other.                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The class name format

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  DEVELOPMENT              PRODUCTION                         โ”‚
โ”‚                                                              โ”‚
โ”‚  _button_a1b2c3          button_button_a1b2c3              โ”‚
โ”‚                                                              โ”‚
โ”‚  Shorter, readable       Includes file name for debugging   โ”‚
โ”‚                                                              โ”‚
โ”‚  Pattern: [name]-[local]-[hash] can override both.          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The global escape hatch

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  button.module.css                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  .button { }              โ†’ scoped                           โ”‚
โ”‚  :global(.external) { }   โ†’ not scoped                       โ”‚
โ”‚                                                              โ”‚
โ”‚  The :global() wrapper exempts the selector.                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Convention*.module.css
Importimport styles from './file.module.css'
Class mapstyles.className
Generated name (dev)_<local>_<hash>
Generated name (prod)<name>_<local>_<hash>
Global selector:global(.class)
Local selector:local(.class)
Configuration key@parcel/transformer-css
Global modemodules.global: true

Key takeaways:

  • The .module.css suffix is the entire configuration for the default behavior. A file with the suffix is a module; a file without it is global. No config file is needed for the scoping to work.
  • The default import returns a class map. The keys are the original class names, and the values are the generated ones. The element must use styles.className, not the original name.
  • The generated class name includes a file-path hash. Two modules with the same class name produce different generated names, so the rules cannot collide.
  • :global is the escape hatch. A rule that must apply to an element from a third-party library can be marked as global. The rest of the module remains scoped.
  • The pattern and the global mode are configurable. The pattern field controls the class name format, and global: true treats every CSS file as a module, not just the ones with the suffix.
  • TypeScript declarations are generated automatically. Parcel produces a .d.ts for the module, so the class map is typed without a manual declaration file.

Remember: A CSS Module is a CSS file whose class names are scoped to the module that imports it. The .module.css suffix enables the scoping, the default import exposes the class map, and the generated names prevent collisions. Use the map, not the original class name. Use :global for third-party selectors. The result is the same CSS syntax with the collision problem solved mechanically rather than by convention.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!