CSS Modules / Scoped stage
Isolation upgrade
CSS Modules / Scoped stage
CSS Modules represent the stage where build tools, not humans, manage scope. Isolation becomes clear, but the abstraction depends more heavily on component structure.
Compile-time hashesLocal scope by defaultThemes and shared variables need separate design
Related packages
Ecosystem tools, libraries, and builder foundations referenced by this page.
Related solutions
Recommended follow-up chapters, supporting methods, and engineering landing points.
Key Points
- Class name hashing implements scope isolation, naturally preventing global pollution; suitable for releasable component libraries.
- Reuse relies on shared variables/mixed files, and cross-component reuse requires additional abstraction; theme switching requires additional token pipelines.
- Suitable for medium and large-scale web applications or component libraries that require isolation but do not want to introduce a runtime.
- Representative packages/toolchains:
webpack css-loader modules,
Advantages / Disadvantages / When to use
| Item | Content |
|---|---|
| Advantages | Good isolation; no runtime; smooth cooperation with React/Vue |
| Disadvantages | Tokens/themes require additional pipelines; cross-component style reuse requires careful abstraction |
| Applicable | Component libraries/applications whose design system has not yet been tokenized but needs to be isolated |
| Not applicable | Teams that need to share atomic classes across platforms (small programs), or want to use Utility-first directly |
Representative packages and usage
- webpack/Vite CSS Modules: Enable
modulesin the bundler to generate hash class names; it can be combined with:globalto expose public styles.
Vite configuration example vite.config.ts
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCase',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
})
- Next.js / CRA supports by default: writing styles in
.module.cssand.module.scssfiles, automatic scope isolation.
Next.js page uses CSS Modules
import styles from './page.module.css'
export default function Page() {
Return <div className={styles.hero}>Next.js brings out-of-box isolation </div>
}
- vanilla-extract: Generate CSS at compile time, expose
classNameand variables, zero runtime, often used in component libraries with high performance requirements.
vanilla-extract
// styles.css.ts
import { style, createVar } from '@vanilla-extract/css'
export const primary = createVar()
export const card = style({
borderRadius: '16px',
vars: { [primary]: '#111827' },
border: `1px solid color-mix(in srgb, ${primary} 10%, transparent)`,
})
Vue <style scoped> (also belongs to the compile-time scope school)
- Principle: When SFC is compiled, add
data-v-xxxxto both the template node and the style selector, so that the style only acts on the DOM rendered by the current component. - Example:
<template>
<section class="card">
<p class="eyebrow">Scoped</p>
<h2>{{ title }}</h2>
</section>
</template>
<style scoped>
.card { @apply rounded-xl border bg-card/80 p-4; }
.eyebrow { @apply text-xs uppercase tracking-[0.2em] text-muted-foreground; }
</style>
- Product:
.card[data-v-xxxx] { ... }, the node is rendered as<section class="card" data-v-xxxx>. - Applicable/Note: Suitable for local isolation of Vue components; if global style or third-party component penetration is required, use
:global/::v-deep; can be superimposed with Tailwindprefixto reduce host coverage.
Svelte <style> default isolation
- Principle: The Svelte compiler generates a unique identifier (such as
svelte-abc123) for the component and attaches it to the DOM and style selector. The effect is similar to scoped. - Example:
<script>
export let title = 'Svelte Scoped'
</script>
<section class="card">
<h2>{title}</h2>
</section>
<style>
.card {
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px;
}
</style>
- Product: After compilation, the node is
<section class="card svelte-abc123">and the CSS is.card.svelte-abc123 { ... }. - Applicable/Note: It can be isolated by default, and the shared style across components requires
:global(.class); if Tailwind is superimposed, you can directly write the atomic class in the template or<style>in@apply(configuration required). Can still be combined withprefix/namespace in micro frontend/embedded scenarios.
Example (React)
Card.tsx
import styles from './Card.module.css'
export function Card() {
return (
<section className={__PROTECTED_0__}>
<div className={styles.header}>
<p className={styles.eyebrow}>CSS Modules</p>
<h2 className={styles.title}>Scope isolation</h2>
<p className={styles.desc}> class name is hashed and will not conflict with the global. </p>
<button className={styles.button}>View details</button>
</div>
</section>
)
}
Card.module.css
.card { border: 1px solid #e5e7eb; border-radius: 16px; padding: 16px; }
.elevated { box-shadow: 0 10px 30px rgba(0,0,0,0.06); }
.header { display: flex; flex-direction: column; gap: 8px; }
.eyebrow { font-size: 12px; letter-spacing: 0.1em; color: #6b7280; }
.title { font-size: 18px; margin: 0; }
.desc { color: #4b5563; font-size: 14px; }
.button { padding: 10px 16px; border-radius: 8px; border: 1px solid #111827; background: #111827; color: #fff; }
Common pitfalls and countermeasures
- Theme/Multiple Brands: Host tokens through CSS variables and then reference them in Modules to reduce global files.
- Reuse: Extract
variables.cssandmixins.cssto avoid repeatedly defining color palettes and spacing in multiple modules. - Dynamic classes: avoid splicing hash classes at runtime, and declare necessary modification classes in the module.
- Cooperate with Utility-first: you can keep a small amount of
@applyin Modules (if configuration allows), or keep atomic helper classes during the transition period.