跳到主要内容

CSS Modules / Scoped 阶段

要点

  • 类名哈希化实现作用域隔离,天然防止全局污染;适合可发布组件库。
  • 复用靠共享变量/混入文件,跨组件复用需要额外抽象;主题切换需额外 token 管线。
  • 适合中大型 Web 应用或组件库,需要隔离但不想引入运行时。
  • 代表性包/工具链:webpack css-loader modulesVite + vite:css-modulesNext.js 内置 CSS Modules 支持、vanilla-extract(零运行时但同属编译期 CSS 模块流派)、Vue <style scoped>(为选择器自动加 data-v-* 的同类方案)、Svelte <style>(自动注入 svelte-xxx 选择器)。

优势 / 劣势 / 何时使用

内容
优势隔离性好;无运行时;与 React/Vue 配合顺畅
劣势tokens/主题需额外管线;跨组件样式复用需要谨慎抽象
适用设计体系尚未 token 化、但需要隔离的组件库/应用
不适用需要跨平台(小程序)共享原子类,或希望直接用 Utility-first 的团队

代表性包与用法

  • webpack / Vite CSS Modules:在 bundler 中开启 modules,生成哈希类名;可结合 :global 暴露公共样式。
Vite 配置示例 vite.config.ts
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCase',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
})
  • Next.js / CRA 默认支持:在 .module.css.module.scss 文件中书写样式,自动作用域隔离。
Next.js 页面使用 CSS Modules
import styles from './page.module.css'

export default function Page() {
return <div className={styles.hero}>Next.js 带来的开箱隔离</div>
}
  • vanilla-extract:编译期生成 CSS,暴露 className 与变量,零运行时,常用于对性能要求高的组件库。
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>(同属编译期作用域流派)

  • 原理:SFC 编译时为模板节点与样式选择器都添加 data-v-xxxx,使样式仅作用于当前组件渲染的 DOM。
  • 示例
<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>
  • 产物.card[data-v-xxxx] { ... },节点渲染为 <section class="card" data-v-xxxx>
  • 适用/注意:适合 Vue 组件局部隔离;若需全局样式或第三方组件穿透,使用 :global/::v-deep;可与 Tailwind prefix 叠加以减少宿主覆盖。

Svelte <style> 默认隔离

  • 原理:Svelte 编译器为组件生成唯一标识(如 svelte-abc123),并将其附着在 DOM 与样式选择器上,效果类似 scoped。
  • 示例
<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>
  • 产物:编译后节点为 <section class="card svelte-abc123">,CSS 为 .card.svelte-abc123 { ... }
  • 适用/注意:默认即可隔离,跨组件共用样式需 :global(.class);如叠加 Tailwind,可直接在模板写原子类或在 <style>@apply(需配置)。在微前端/嵌入场景仍可与 prefix/命名空间组合。

示例(React)

Card.tsx
import styles from './Card.module.css'

export function Card() {
return (
<section className={`${styles.card} ${styles.elevated}`}>
<div className={styles.header}>
<p className={styles.eyebrow}>CSS Modules</p>
<h2 className={styles.title}>作用域隔离</h2>
<p className={styles.desc}>类名哈希化,不会与全局冲突。</p>
<button className={styles.button}>查看详情</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; }

常见坑与对策

  • 主题/多品牌:通过 CSS 变量承载 tokens,再在 Modules 中引用,减少全局文件。
  • 复用:抽出 variables.cssmixins.css,避免在多个模块重复定义色板与间距。
  • 动态类:避免在运行时拼接哈希类,统一在模块里声明必要的修饰类。
  • 与 Utility-first 协作:可在 Modules 内少量 @apply(若配置允许),或在过渡期同时保留 atomic 辅助类。