🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
CSS MASTER CLASS /// VISUAL ENGINEERING /// LAYOUT DESIGN /// ANIMATION LAB /// CSS MASTER CLASS /// VISUAL ENGINEERING ///

CSS Modules: Structural, Not Conventional, Scoping

Learn how CSS Modules solve global scoping structurally through automatic build-time class name hashing, the :global() escape hatch for deliberately targeting genuinely global selectors, and how the composes keyword shares styles between local classes without relying on the cascade.

Total XP: 0|💻 css XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

CSS Modules

Structural, not conventional, scoping.


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Every methodology in the Architecture module — BEM, ITCSS, SMACSS — solves CSS's scoping problem through disciplined convention, something humans have to get right every time. CSS Modules take a fundamentally different approach: making the build tool do it automatically.

1Structural Scoping Through Automatic Hashing

A file named with the .module.css convention (or configured equivalent) is processed by a build step that scans every class selector and rewrites it into a name guaranteed unique across the entire build — typically incorporating the component's filename and a content hash, like .Card_card__a1b2c. The build tool also emits a JavaScript object (often accessed by importing the CSS file directly) mapping your original, readable class name (card) to the generated hashed one, so your component code references styles.card and never needs to know or hardcode the actual hash.

This is a genuinely different kind of safety than BEM's naming convention: BEM reduces collision risk through disciplined naming that developers have to apply correctly and consistently; CSS Modules eliminate collision risk structurally, since two completely differently-authored components can both use the literal class name .card with zero actual collision, because the build tool guarantees each gets a distinct hashed output.

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

// Card.jsx
import styles from './Card.module.css';
// "Card_card__a1b2c"
localhost:3000
✓ Structurally Collision-ProofTwo components can both write .card and never collide — the build tool guarantees uniqueness mechanically, not through convention.

2Local By Default, Global As A Deliberate Choice

CSS Modules' default behavior — every class scoped locally and hashed — covers the overwhelming majority of real component styling needs. But some cases genuinely require targeting a real, unhashed global class name: styling markup rendered by a third-party library whose class names you don't control and can't hash, or intentionally applying a style globally across the whole application.

:global(.some-class) is the explicit, deliberate escape hatch for exactly this — it tells the build tool 'don't hash this specific selector, treat it as a genuine global class name'. Because this is an explicit, visible marker in the source rather than an accidental default, it keeps the intentional global exceptions clearly distinguishable from the safely-scoped majority of a file's rules.

.card { } /* local, hashed automatically */
:global(.third-party-widget) { color: red; } /* real global class, unhashed */
localhost:3000
Default: safely scoped and hashed
:global(): explicit, visible exception

3composes: Sharing Styles Without The Cascade

The composes keyword lets a local class declare that it also includes another class's styles — composes: button; inside .primaryButton doesn't merge or inherit CSS declarations the way Sass's @extend or the cascade might; instead, the build tool resolves it into applying *both* hashed class names (button's and primaryButton's) together on the actual rendered element. Both rule sets' styles genuinely apply, completely independent of any specificity or source-order consideration, since it's not going through the cascade at all — it's a build-time composition of which classes end up on the element.

This makes composes a genuinely different, arguably cleaner sharing mechanism than cascade-based approaches: there's no risk of a specificity conflict between the composed and composing styles, since both are simply applied as separate, co-existing classes on the same element, exactly as if you'd manually written className="button primaryButton" yourself, but generated safely and automatically by the build tool.

.button { padding: 12px 20px; border-radius: 6px; }
.primaryButton {
  composes: button;
  background: blue;
}
localhost:3000
✓ No Cascade, No Specificity RiskBoth hashed classes are applied together on the element — composition without any specificity conflict risk.

4Step-by-Step Breakdown

Build-Time Scoping, Automated. BEM solves CSS's global scoping problem through naming discipline — a convention humans have to follow correctly, every time. CSS Modules solve the exact same problem differently: a build tool automatically rewrites every class name into something guaranteed unique, making naming collisions structurally impossible rather than merely unlikely.

Automatic Class Name Hashing. A CSS Modules build step transforms .card { } written in a .module.css file into something like .Card_card__a1b2c at build time, and generates a corresponding JavaScript object mapping your original class name to the hashed one — you import and use styles.card in your component, never needing to know or write the actual generated hash yourself.

How Class Hashing Works. Why does automatic class name hashing make naming collisions structurally impossible, rather than just less likely?

  • It's still just a naming convention, so collisions remain possible if not followed correctly
  • The build tool generates a genuinely unique hash for every class, tied to its specific file and declaration, so two components can use the identical original class name with zero actual collision risk
  • There's no real difference from a naming convention like BEM

Local Scope By Default, Global When Needed. Every class in a .module.css file is scoped locally by default — hashed and unreachable from outside its own component's JavaScript import. The :global() escape hatch explicitly opts a specific selector out of this scoping when you genuinely need to target something outside the module's own boundary, like styling a third-party library's markup.

The :global() Escape Hatch. What does wrapping a selector in :global() inside a CSS Module file do?

  • It gives the rule !important priority
  • It explicitly opts that specific selector out of the automatic local hashing, targeting a real, unhashed global class name instead
  • It has no functional effect, purely documentation

composes: Sharing Styles Without Duplication Or Cascade Reliance. The composes keyword lets one local class inherit another local class's (or an imported class's) styles directly, resolved at build time into the final class list applied to the element — a form of style composition that doesn't rely on the cascade or inheritance at all, avoiding the specificity concerns those mechanisms can introduce.

Understanding composes. What does composes: button; inside .primaryButton actually produce in the final rendered HTML?

  • primaryButton's CSS rule set inherits button's declarations via the cascade
  • Both the hashed .button and .primaryButton class names get applied together on the element, so both rule sets' styles apply
  • It renames .primaryButton to .button entirely

CSS Modules Mastered. You now understand how CSS Modules solve global scoping structurally through automatic build-time class hashing, how :global() provides a deliberate escape hatch for the rare genuinely-global case, and how composes shares styles between local classes without relying on the cascade or inheritance.

Style A Module-Scoped Class. CSS Modules append a unique suffix to each class name to guarantee it's scoped to one component.

Level Up 🚀

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1Automatic Class Hashing Doesn't Affect Any Accessibility-Relevant HTML Attributes, Only Class Names

CSS Modules only transform class selectors — ARIA attributes, roles, and IDs used for accessibility purposes (like aria-labelledby references) are untouched by the hashing process and need their own, separate correctness verification.

2The :global() Escape Hatch Is Sometimes Necessary For Styling Third-Party Accessible Widget Libraries Correctly

Many accessible component libraries (date pickers, comboboxes) render with fixed, documented global class names that need :global() to target and customize correctly within a CSS Modules-based project.

SEO Implications

  • 1

    CSS Modules' Structural Scoping Reduces The Total Bug Surface Related To Accidental Style Leakage On Large Sites

    Fewer accidental cross-component style collisions mean fewer emergency visual-bug fixes, supporting more consistent, uninterrupted engineering velocity on ongoing content and performance work.

  • 2

    Unused CSS Modules Classes Are Generally Easier To Detect Automatically Than Unscoped Global Classes

    Because CSS Modules classes are only referenced via explicit JavaScript imports, tooling can more reliably trace usage and detect genuinely dead code compared to auditing loosely-scoped global class usage across an entire codebase.

Best Practices

Default To Local Scoping And Reserve :global() Explicitly For Genuinely Necessary Cases

This keeps the intentional exceptions visible and deliberate in the source code, rather than allowing global scoping creep back in as an unconsidered default.

Use composes For Sharing Styles Between Local Classes Instead Of Reaching For Sass @extend Or Cascade-Based Inheritance

It avoids any specificity conflict risk entirely, since composition happens at the class-application level rather than through cascade resolution.

Frequent Bugs

THE BUG

A style intended to target a third-party library's markup isn't applying inside a CSS Modules file.

THE FIX

The selector needs to be wrapped in :global() so the build tool doesn't hash it — the third-party library's actual class name in the DOM is unhashed, so a hashed selector can never match it.

THE BUG

A composed style's declarations don't seem to override the base class's declarations as expected.

THE FIX

Remember composes applies both classes together, not through the cascade — if truly needing an override rather than addition, either restructure to avoid needing a cascade-order-dependent override, or don't rely on composes for that specific case.

Real-World Examples

A Composed Button Variant System

A component library building primary, secondary, and danger button variants that all compose a shared base button class, avoiding duplicated padding/border-radius declarations across variants.

.button { padding: 12px 20px; border-radius: 6px; font-weight: 600; }
.primaryButton { composes: button; background: var(--color-action-primary); }
.dangerButton { composes: button; background: var(--color-danger); }

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Trying to target a third-party library's class name without :global()

/* Wrong: gets hashed, never matches */ .third-party-widget { color: red; } /* Correct */ :global(.third-party-widget) { color: red; }

The Solution //

Wrap the selector in :global() so it isn't hashed and can actually match the real, unhashed class name in the DOM.

The Error //

Expecting composes to work like Sass @extend with cascade-based override behavior

/* Applies BOTH .button and .primaryButton classes to the element */ .primaryButton { composes: button; }

The Solution //

Remember composes applies both classes together on the element rather than merging declarations through the cascade.

Lesson Glossary

[01]CSS Modules

A build-time technique automatically scoping CSS class names.

Code Preview
.module.css

[02]Class Hashing

Automatically rewriting class names into guaranteed-unique identifiers.

Code Preview
Card_card__a1b2c

[03]:global()

An escape hatch opting a selector out of automatic local scoping.

Code Preview
:global(.third-party)

[04]composes

A keyword sharing styles between local classes without the cascade.

Code Preview
composes: button;

Continue Learning