跳到主要内容

CSS Modules

CSS Modules 解决的问题很直接:CSS 默认是全局作用域,组件一多,类名冲突、样式泄漏、覆盖顺序失控这些问题就会越来越明显。

它的做法也很直接:把 .module.css.module.scss 这类文件里的类名做局部化处理,再通过 JavaScript 模块把映射关系导出来。

所以它不是浏览器原生特性,也不是某个框架独占能力,而是一种构建期约定。

先看 CSS Modules 在解决什么

传统全局 CSS 最常见的问题通常是:

  • .button.title.card 这类类名很容易撞车
  • 页面越来越多之后,很难判断某个类名还会不会影响别处
  • 重构组件时,不容易确认哪些样式还能删
  • 样式依赖关系不清,最后只能靠命名规范硬撑

CSS Modules 的核心思路一般就是:

  • 默认把类名局部化
  • 让样式文件和组件文件形成一对一或一对少的依赖关系
  • 通过 import 明确当前组件到底在用哪份样式

一个最小例子

/* button.module.css */
.root {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 0.5rem 1rem;
background: #111827;
color: white;
}
import styles from './button.module.css'

export function Button() {
return <button className={styles.root}>提交</button>
}

构建之后,root 不会直接原样落到最终产物里,而是会被处理成带 hash 的类名。

它的运行方式怎么理解

导入 CSS Module 时,打包器通常会导出一个对象:

{
root: 'button_root__3X9fA'
}

组件里拿到的 styles.root,其实就是这个映射后的类名。

这一层很重要,因为它说明了两件事:

  • CSS Modules 的局部作用域是构建期行为
  • 最终进入浏览器的,仍然是普通 CSS

CSS Modules 最常见的特性

1. 局部作用域

.title {
font-size: 1.25rem;
font-weight: 600;
}

在模块文件里,这个 .title 默认是局部的,不会直接污染整个站点。

2. 导出 class 映射

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

export function Card() {
return <article className={styles.title}>标题</article>
}

3. 还能配合预处理器使用

  • button.module.scss
  • card.module.less
  • layout.module.styl

也就是说,CSS Modules 解决的是“作用域”,预处理器解决的是“写法体验”,这两层可以叠在一起。

4. :global:local

默认情况下,模块里的类名是局部的;但有些场景还是得逃回全局。

:global(.markdown-body h1) {
margin-bottom: 1rem;
}
.wrapper :global(.third-party-button) {
border-radius: 999px;
}

如果需要显式声明局部,也能写:

:local(.card) {
padding: 1rem;
}

不过真实项目里,更常见的是默认局部,按需 :global

5. 组合(composes

CSS Modules 支持把一个类组合进另一个类。

.base {
display: inline-flex;
align-items: center;
}

.primary {
composes: base;
background: #2563eb;
color: white;
}

这类写法有用,但很多团队不会把它当主线能力,因为:

  • 可读性不一定比直接拆组件更好
  • 跨文件组合会让依赖变绕

所以它更像“可用能力”,不是所有项目都要大量使用。

在 React / Vite 里怎么接

Vite 官方文档里已经把 CSS Modules 作为内建能力支持。

命名方式

button.module.css
card.module.scss
layout.module.less

基础写法

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

export function Card() {
return (
<section className={styles.card}>
<h2 className={styles.title}>标题</h2>
</section>
)
}

Vite 配置示例

import { defineConfig } from 'vite'

export default defineConfig({
css: {
modules: {
localsConvention: 'camelCaseOnly',
},
},
})

如果样式类名里使用了 kebab-case,例如 .card-title,这项配置会更顺。

.card-title {
font-weight: 600;
}
import { cardTitle } from './card.module.css'

在 Next.js 里怎么接

Next.js 对 CSS Modules 的支持也属于开箱即用。

最常见写法

/* app/dashboard/page.module.css */
.page {
padding: 24px;
}
import styles from './page.module.css'

export default function Page() {
return <main className={styles.page}>Dashboard</main>
}

这也是 Next.js 官方非常常见的样式组织方式之一。

在 Vue 项目里怎么看

Vue 项目里更常见的主线其实是:

  • <style scoped>
  • CSS Modules

两者都在解决作用域问题,但方式不同。

CSS Modules 写法

<template>
<div :class="$style.card">
<h2 :class="$style.title">标题</h2>
</div>
</template>

<style module>
.card {
padding: 16px;
}

.title {
font-weight: 600;
}
</style>

这类写法在 Vue 里不是唯一主线,但在一些需要显式 class 映射的场景会很清楚。

scoped、BEM、Tailwind 的区别

CSS Modules vs scoped

  • scoped:更偏 Vue 单文件组件语义
  • CSS Modules:更偏构建器层面的 class 映射

CSS Modules vs BEM

  • BEM 主要解决命名冲突
  • CSS Modules 直接通过局部作用域减少命名冲突

用了 CSS Modules 之后,通常就不需要把命名写得像 page__header--active 那么重。

CSS Modules vs Tailwind

  • CSS Modules:适合组件局部样式
  • Tailwind:适合 utility-first 和 token 驱动的样式体系

很多项目会混着用:

  • 布局、间距、响应式用 Tailwind
  • 一些复杂组件样式或第三方覆盖用 CSS Modules

一个更接近真实项目的目录结构

src/
components/
Button/
index.tsx
button.module.scss
Card/
index.tsx
card.module.css

这种结构的好处很直接:

  • 组件和样式靠得近
  • 删除组件时,样式一起删更容易
  • 不需要在全局样式目录里来回跳

常见写法模式

1. 一个组件一份样式模块

这是最稳的起步方式。

2. 组件变体配合 clsx

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

export function Button({ primary = false }: { primary?: boolean }) {
return (
<button
className={clsx(styles.button, primary && styles.primary)}
>
提交
</button>
)
}

3. 配合 SCSS 组织 token 和 mixin

@use '@/styles/tokens.scss' as *;

.card {
border-radius: $radius-md;
padding: $space-4;
}

这类组合在 React / Vite / Next 项目里都很常见。

常见误区

1. 以为 CSS Modules 就完全没有全局样式了

不是。

  • reset
  • typography
  • markdown
  • 主题变量
  • 第三方库覆盖

这些通常还是需要全局层。

2. 把所有页面都拆成很多 .module.css

如果项目更适合 Tailwind、CSS variables 或设计系统组件,不一定要把每一个视觉细节都拆进模块文件里。

3. 过度依赖 :global

一旦 :global 到处都是,CSS Modules 的主要价值会被削弱。

4. 继续用很重的 BEM 命名

既然已经局部化了,命名可以回到更直接的层级,比如:

  • .card
  • .title
  • .footer

什么时候值得优先考虑 CSS Modules

  • React / Next 项目里想保持组件和样式强绑定
  • 不想引入完整 CSS-in-JS
  • 不准备全面走 Tailwind
  • 团队希望作用域明确,但又想保留普通 CSS 写法

什么时候未必是第一选择

  • 项目已经全面走 Tailwind
  • Vue 项目里 <style scoped> 已经足够
  • 设计系统更适合 CSS variables + utility classes 的主线

一个更实际的判断

CSS Modules 不是“比所有样式方案都先进”的答案,它更像一种非常稳的中间路线:

  • 保留 CSS 的直观写法
  • 解决全局污染
  • 保持组件样式边界清晰

这也是它到现在仍然很常用的原因。

参考来源