CSS Modules: How to Scope Styles
Learn how CSS Modules map local class names, use global selectors and composition, and avoid common cascade and framework pitfalls.
CSS Modules scope class selectors by treating them as local to a stylesheet and mapping them to generated class names during the build. Import the module, then use its exported mapping in your markup. Two files can each define .button without those local class names colliding.
This is build-time selector naming, not browser-level isolation: global selectors, inheritance, custom properties, and the CSS cascade still apply. CSS Modules work through a build integration and are not limited to React. The CSS Modules documentation describes the local mapping model and compilation to ICSS.
1. Create and use a CSS Module
With a configured CSS Modules integration, name a stylesheet according to your framework’s convention, define ordinary CSS classes, and import the stylesheet into the code that uses them.
/* Card.module.css */
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
.title {
font-weight: 700;
margin: 0;
}
// Card.jsx
import styles from './Card.module.css';
export function Card() {
return (
<article className={styles.card}>
<h2 className={styles.title}>Title</h2>
</article>
);
}
The imported object maps local names such as card and title to generated names emitted by the build. Use styles.card in markup; do not hard-code the generated spelling, which is an implementation output and may change between builds or configurations.
What is scoped?
Local class selectors are the central scoping mechanism. A class named button in Card.module.css and another named button in Dialog.module.css receive separate mappings when used through their respective imported objects.
The mapping does not isolate the entire stylesheet from the document. Element selectors, inherited properties, CSS custom properties, global rules, specificity, and stylesheet order can still affect the result. CSS Modules are not Shadow DOM and do not create a runtime security boundary.
2. Configure CSS Modules for your framework
CSS Modules require a build tool or framework integration that understands the module file and returns a class mapping. Check that integration before changing CSS syntax or debugging generated names.
Next.js
Next.js uses the .module.css filename convention and imports the module as a styles object. Its global CSS conventions depend on the router and framework version. In the Pages Router, follow the guidance for placing site-wide global CSS at the application root; in the App Router, the documentation permits global CSS imports in layouts, pages, or components. Import order can affect the resulting CSS, so consult the docs for the router and version in use.
See the Next.js Pages Router CSS documentation and Next.js App Router CSS documentation.
Other build integrations
Do not assume every bundler uses the same filename pattern, import behavior, or local name generation settings. Use your framework’s CSS Modules setup and verify that the import resolves to a mapping. The CSS Modules project documentation explains the module model and its ICSS compilation: CSS Modules documentation.
3. Add a deliberate global selector
Use the documented :global(...) escape when a module needs to target a global hook, such as a class supplied by a vendor or another part of the application.
/* Widget.module.css */
:global(.vendor-widget) {
font-family: system-ui, sans-serif;
}
.root {
padding: 1rem;
}
Here, .root remains local, while .vendor-widget is intentionally global. Keep global selectors narrow and explicit. The project documentation also describes a :global selector form; check the syntax supported by your integration when using it. See CSS Modules global and composition documentation.
4. Compose local classes
composes lets one local class include class names from another local selector, including a class in a different module. The exported mapping for the composed class includes both names.
/* shared.module.css */
.primary {
color: white;
background: navy;
}
/* Button.module.css */
.button {
composes: primary from './shared.module.css';
padding: 0.5rem 1rem;
border: 0;
}
import styles from './Button.module.css';
export function Button() {
return <button className={styles.button}>Save</button>;
}
Follow these constraints from the CSS Modules documentation:
- Composition applies to a single local class selector.
- Put composition declarations before the selector’s other declarations.
- Avoid circular composition dependencies. Their override behavior is undefined and they may cause an error.
For small components, composition can share class behavior while keeping styles in CSS. Use it where the relationship is clear; do not rely on it to make cascade or override behavior predictable across a complex dependency chain.
5. Choose a useful module boundary
A practical starting point is one module per component or cohesive UI area. Keep local class names readable and use the exported object at the point where the component renders. Put app-wide resets, typography, and intentional global hooks in the framework’s designated global stylesheet location.
When deciding whether a style belongs in a module, ask:
- Does this class describe a component or visual region that should be locally referenced?
- Is this selector an intentional integration point that must remain global?
- Could inheritance, a custom property, a global element rule, specificity, or import order explain the observed styling?
- Does the framework require a particular file name or import location?
These checks preserve the main benefit—avoiding local class-name collisions—without implying that every CSS effect becomes isolated.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
styles is undefined or the stylesheet import fails |
The file name or build configuration is not recognized as a CSS Module. | Use the framework’s module naming convention, such as .module.css in Next.js, and check its CSS Modules setup. |
| The class is missing from the rendered element | The code uses a literal class string instead of the mapping, or the local class name is misspelled. | Use className={styles.card} and confirm the selector is .card in the imported module. |
| A class name appears different in generated HTML | The build has mapped the local name to its generated name. | This is expected. Reference the mapping rather than copying the generated name into markup or another stylesheet. |
| A vendor or global class rule has no effect | The selector is being treated as local, or it does not match the global element. | Use the integration’s documented :global(...) syntax for that deliberate global selector and verify the rendered class. |
| A style changes after moving imports or between development and production | CSS order or cascade interactions affect which declaration wins. | Follow your framework’s import placement guidance, reduce unintended global rules, and inspect specificity and source order. |
| A composed class does not include the expected style | The source path, local selector, or composition placement is invalid; a dependency may also be circular. | Check the imported module path and selector, put composes before other declarations, and remove circular composition. |
| An element still inherits an unexpected font, color, or custom property | Local class mapping does not stop CSS inheritance or global custom properties. | Trace inherited values and define the intended value on the component where appropriate. |
7. Performance, reliability, and cost
CSS Modules provide a naming and build organization convention; the cited documentation does not establish a universal runtime performance gain or a numeric overhead. Generated class names are build output, so keep code coupled to the exported mapping rather than their spelling. Build and deployment behavior depends on the framework and configuration.
For reliable styling, keep global exceptions deliberate, avoid circular composition, and follow router-specific CSS import rules. When a production appearance differs from development, check emitted stylesheet order and global interactions before assuming local class mapping failed.
The CSS Modules mechanism itself is a project/build convention, not a paid service. Costs depend on the framework, build infrastructure, and any separate tools used by the project; this research does not provide benchmark or pricing figures.
8. Inspect the rendered result with a screenshot
After changing CSS, capture the rendered page at the viewport and state where the problem appears. A screenshot can make clipping, spacing, and responsive differences easier to spot. For a DIY capture workflow, use your existing browser automation or a screenshot API; CSS Modules does not require a particular screenshot tool.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Cookie banners are accepted like a visitor and removed, along with known consent platforms, newsletter popups, and chat widgets, before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; the response identifies the page verdict and billing status. AI agents can use its MCP tools, including take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation. Example cURL request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo includes full-page and selector capture, viewport and device presets, retina scale, dark mode, custom CSS and JavaScript, waits, request blocking, custom headers and cookies, caching, async jobs, bulk capture, signed links, and a usage API. It bills only clean shots. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.
Learn about ScreenshotNeo, or sign up for 1,000 free screenshots a month with no card.
9. FAQ
Are CSS Modules specific to React?
No. The mapping convention is not tied to React, though frameworks differ in how they configure and expose it.
Do CSS Modules make styles fully isolated?
No. They map local class selectors. Global rules, inheritance, custom properties, and cascade order can still affect the page.
Should I use generated class names in tests or external CSS?
Prefer the module mapping in application code. Generated spellings are build output rather than a stable authoring interface.
Where should global CSS go in Next.js?
It depends on whether the project uses the Pages Router or App Router and on the framework version. Follow the matching Next.js documentation for global CSS placement.


