13 — CSS Modules 与项目样式

10 分钟理解项目里所有 CSS 的写法。
包括:CSS Modules 工作机制、设计令牌系统、6 主题切换原理、常用模式。


1. CSS Modules 是什么?

CSS Modules 是一种文件命名约定,文件名包含 .module.css 就会被 Vite 处理。

1
2
3
/* 任何 *.module.css 文件:类名会被 Vite 自动哈希 */
.item { ... } /* 编译为 .Message_item_a1b2c3 */
.active { ... } /* 编译为 .Message_active_x7y8z9 */

核心价值

  • ✅ 类名永远不会冲突
  • ✅ 每个组件样式独立管理
  • ✅ 不需要 BEM 等命名规范
  • ✅ 不需要额外构建配置

详见 03-CSS基础回顾.md 第 9 节。


2. 项目里所有 CSS 文件清单(最新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
src/
├── styles/
│ ├── tokens.css ← 🎨 设计令牌(6 主题分组,data-theme 切换)⭐
│ └── global.css ← 🌐 全局 reset、滚动条、背景水墨

├── App.module.css ← 根布局(position: fixed + safe-area)

└── components/
├── Sidebar/
│ ├── Sidebar.module.css
│ └── ChatItem.module.css
├── MessageList/
│ ├── MessageList.module.css
│ └── EmptyState.module.css
├── Message/
│ ├── Message.module.css
│ ├── Markdown.module.css
│ ├── Timeline.module.css
│ ├── RecommendQuestions.module.css
│ ├── ReferenceList.module.css
│ └── PptDownload.module.css
├── InputArea/
│ ├── InputArea.module.css
│ ├── AgentSelector.module.css
│ └── FilePreview.module.css
├── Topbar/ ← 🆕
│ └── Topbar.module.css
├── ThemeSwitcher/ ← 🆕
│ └── ThemeSwitcher.module.css
├── ConfirmDialog/
│ └── ConfirmDialog.module.css
└── ConnectionError/
└── ConnectionError.module.css

规则:每个 .tsx 组件对应一个 .module.css(除了简单的子组件)。


3. 设计令牌系统(tokens.css)

文件src/styles/tokens.css6 主题分组

3.1 为什么需要设计令牌?

1
2
3
4
5
6
7
8
9
/* ❌ 不用令牌 */
.button { background: #e8a548; padding: 12px; border-radius: 10px; }
.card { background: #141311; padding: 16px; border-radius: 10px; }
/* 颜色值散落各处,改主题要改 N 个地方 */

/* ✅ 用令牌 */
:root { --color-primary: #e8a548; --bg-surface: #141311; --space-3: 12px; }
.button { background: var(--color-primary); padding: var(--space-3); }
.card { background: var(--bg-surface); padding: var(--space-3); }

3.2 全局令牌(所有主题共享)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
:root {
/* ── 字体 ── */
--font-sans: 'Plus Jakarta Sans', ...;
--font-display: 'Sora', ...; /* 用于标题 */
--font-mono: 'JetBrains Mono', ...;

/* ── 间距(4px 基准)── */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;

/* ── 字体大小 ── */
--text-xs: 11px;
--text-sm: 12.5px;
--text-base: 14px;
--text-md: 15px;
--text-lg: 17px;
--text-xl: 20px;
--text-2xl: 24px;

/* ── 圆角 ── */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 18px;
--radius-full: 9999px;

/* ── 布局 ── */
--sidebar-width: 280px;
--max-content-width: 760px;
--topbar-height: 56px;

/* ── 过渡 ── */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--transition-fast: 120ms ease;
--transition-base: 250ms var(--ease-out);
}

3.3 主题令牌(按主题分组覆盖)

关键原理:用 [data-theme='xxx'] 选择器分组覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* ── 默认主题:墨韵东方 (ink) ── */
:root,
[data-theme='ink'] {
--bg-base: #0e0d0b;
--bg-surface: #161412;
--bg-elevated: #1d1a16;
--color-primary: #c9a36b; /* 古铜金 */
--color-primary-soft: rgba(201, 163, 107, 0.12);
--text-primary: #d4cab8; /* 宣纸米 */
--border-subtle: rgba(212, 202, 184, 0.08);
--shadow-glow: 0 0 20px rgba(201, 163, 107, 0.15);
}

/* ── 亮色主题:素雅米黄 (paper) ── */
[data-theme='paper'] {
--bg-base: #f5f0e6; /* 宣纸色 */
--bg-surface: #fbf7ee;
--bg-elevated: #ffffff;
--color-primary: #a07a3e; /* 古铜金(暗一档适配亮色)*/
--text-primary: #2a241a; /* 深墨文字 */
--border-subtle: rgba(42, 36, 26, 0.06);
--shadow-glow: 0 0 20px rgba(160, 122, 62, 0.1);
}

/* ── 其他 4 个主题类似... ── */

3.4 主题切换原理

1
2
// 切换主题只做一件事
document.documentElement.setAttribute('data-theme', 'paper');

CSS 选择器 [data-theme='paper'] 自动匹配,所有 --xxx 变量立即应用,所有组件不需改动

详见 16-主题系统与ThemeSwitcher.md

3.5 如何使用

1
2
3
4
5
6
7
8
9
10
/* 任何 .module.css 文件里直接用 var() */
.myComponent {
color: var(--text-primary);
background: var(--bg-surface);
padding: var(--space-4);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
font-family: var(--font-display);
transition: all var(--transition-base);
}

3.6 怎么加新主题?

tokens.css 末尾加一个 [data-theme='newtheme'] 块,覆盖需要的变量即可:

1
2
3
4
5
6
7
[data-theme='mytheme'] {
--bg-base: #1a1a2e;
--bg-surface: #252540;
--color-primary: #ff79c6;
--text-primary: #f8f8f2;
/* 其他需要的变量... */
}

然后在 themeStore.tsTHEMES 数组加一行:

1
{ id: 'mytheme', name: '我的主题', description: '...', swatch: '#1a1a2e', mode: 'dark' },

UI 自动显示新主题选项。


4. 全局样式(global.css)

文件src/styles/global.css

4.1 包含什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* 1. CSS Reset */
*, *::before, *::after { box-sizing: border-box; }

/* 2. 真实视口绑定(关键:不用 100vh!)*/
html, body, #root {
position: fixed;
inset: 0;
overflow: hidden;
}

/* 3. body 默认样式 */
body {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--text-primary);
background: var(--bg-base);
overscroll-behavior: none; /* 防 iOS 橡皮筋 */
-webkit-overflow-scrolling: touch;
}

/* 4. 滚动条自定义 */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: var(--border-default); }

/* 5. 背景水墨装饰(被 App.tsx 用 div.bg-blob 引用)*/
.bg-blob {
position: fixed;
border-radius: 50%;
filter: blur(180px);
}
[data-theme='ink'] .bg-blob-1 {
background: var(--color-primary-glow);
opacity: 0.15;
}

/* 6. iOS 安全区变量(供组件引用) */
html {
--sat: env(safe-area-inset-top);
--sab: env(safe-area-inset-bottom);
}

/* 7. 选中文本高亮 */
::selection {
background: var(--color-primary-soft);
color: var(--text-primary);
}

4.2 为什么用 position: fixed; inset: 0 不用 100vh

详见 17-移动端适配实战.md。一句话:Chrome 移动端 100vh 含地址栏高度,导致底部输入框被盖住


5. 组件 CSS Module 通用模式

5.1 容器 + 内部元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.message {
display: flex;
gap: var(--space-3);
margin-bottom: var(--space-6);
animation: fadeIn 300ms var(--ease-out);
}
.avatar {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 50%;
}
.user .avatar {
background: var(--color-primary-soft);
color: var(--color-primary);
}

5.2 条件样式的两种写法

1
2
3
4
5
// 写法 1:JS 里三元拼接(项目主流)
<div className={`${styles.item} ${active ? styles.active : ''}`}>

// 写法 2:CSS 里写完整 selector(多类名组合时用)
<div className={`${styles.item} ${active ? styles.active : ''}`}>
1
.item.active { background: var(--color-primary-soft); }

5.3 动画

1
2
3
4
5
6
7
8
9
10
/* 入场 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.message { animation: fadeIn 300ms var(--ease-out); }

/* Hover 微交互 */
.btn { transition: all var(--transition-base); }
.btn:hover { background: var(--color-primary-strong); transform: scale(1.05); }

5.4 透明磨砂胶囊(新出现的模式)

1
2
3
4
5
6
7
8
9
.controls {
background: color-mix(in srgb, var(--bg-elevated) 75%, transparent);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-full);
}
/* color-mix:把背景色和透明色按 75%/25% 混合 */
/* backdrop-filter:对背后的内容做模糊 */

5.5 响应式断点

1
2
3
4
5
6
7
8
9
10
11
12
/* 桌面端默认 */
.sidebar { position: relative; width: var(--sidebar-width); }

/* 移动端:≤ 768px */
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0; left: 0; bottom: 0;
transform: translateX(-100%); /* 隐藏 */
}
.sidebar.open { transform: translateX(0); } /* 显示 */
}

6. 项目里常见的 CSS 模式

6.1 完全居中

1
2
3
4
5
.center {
display: flex;
align-items: center;
justify-content: center;
}

6.2 文本单行省略

1
2
3
4
5
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

6.3 横向滚动(移动端常见)

1
2
3
4
5
6
7
.scrollX {
display: flex;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
}
.scrollX::-webkit-scrollbar { display: none; }

6.4 金色左竖线(AI 消息标识)

1
2
3
4
5
6
7
8
9
10
11
12
.aiBlock {
position: relative;
padding-left: 16px;
}
.aiBlock::before {
content: '';
position: absolute;
left: 0; top: 6px; bottom: 6px;
width: 2px;
background: linear-gradient(180deg, var(--color-primary), transparent);
border-radius: 0 2px 2px 0;
}

6.5 发光按钮

1
2
3
4
5
6
7
8
9
.btn {
background: var(--color-primary);
transition: all var(--transition-fast);
}
.btn:hover:not(:disabled) {
background: var(--color-primary-strong);
box-shadow: var(--shadow-glow);
transform: scale(1.05);
}

7. 实践:常见样式修改场景

场景 1:改全局主色

1
2
3
4
5
6
7
8
/* tokens.css */
:root,
[data-theme='ink'] {
--color-primary: #e6b455; /* 改成想要的颜色 */
--color-primary-soft: rgba(230, 180, 85, 0.12);
--color-primary-strong: #f0c578;
--shadow-glow: 0 0 20px rgba(230, 180, 85, 0.15);
}

场景 2:让发送按钮变大

1
2
3
4
5
/* InputArea.module.css */
.sendBtn {
width: 48px; /* 36 → 48 */
height: 48px;
}

场景 3:改侧边栏宽度

1
2
/* tokens.css */
--sidebar-width: 320px; /* 280 → 320 */

场景 4:加新主题

1
2
3
4
5
6
7
8
9
10
11
/* tokens.css 末尾添加 */
[data-theme='sunset'] {
--bg-base: #fef3e2;
--bg-surface: #fff8eb;
--color-primary: #ff6b35;
--color-primary-soft: rgba(255, 107, 53, 0.1);
--color-primary-strong: #ff5722;
--text-primary: #2d1810;
--border-subtle: rgba(45, 24, 16, 0.06);
--shadow-glow: 0 0 20px rgba(255, 107, 53, 0.15);
}
1
2
// src/store/themeStore.ts
{ id: 'sunset', name: '落日橘', description: '...', swatch: '#fef3e2', mode: 'light' },

UI 自动显示新主题。


8. 怎么读懂别人的 CSS Module

读 CSS 的顺序

  1. 看类名(命名一般能猜出作用)
  2. 看 display / position(布局方式)
  3. 看 padding / margin / gap(间距)
  4. 看 background / color / border(外观)
  5. 看 :hover / :focus / @keyframes(交互)
  6. 看 @media(响应式断点)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.chatItem {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
cursor: pointer;
color: var(--text-secondary);
font-size: var(--text-sm);
transition: all var(--transition-fast);
}
.chatItem:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.chatItem.active {
background: var(--color-primary-soft);
color: var(--text-primary);
border: 1px solid var(--color-primary);
}

翻译:横向 flex 居中、圆角矩形、默认次级文字、悬停变亮、选中态金色高亮。


9. 常见 CSS 工具/技巧

9.1 :global() 覆盖第三方

1
.markdown :global(.hljs) { background: transparent; }

9.2 data-* 自定义属性

1
[data-active="true"] { background: gold; }

9.3 color-mix() 颜色混合

1
2
background: color-mix(in srgb, var(--bg-elevated) 75%, transparent);
/* 把背景色和不透明度混合,得到半透明色 */

9.4 aspect-ratio 等比

1
.square { aspect-ratio: 1; }

9.5 clamp() 响应式字体

1
font-size: clamp(14px, 2vw, 24px);

10. 一段话总结

项目样式系统:

  1. 设计令牌tokens.css)按主题分组,CSS 变量统一管理
  2. CSS Modules 隔离每个组件样式,类名自动哈希
  3. 全局样式global.css)只放真正全局的东西
  4. 命名用语义.item.title.deleteBtn
  5. 嵌套用 CSS 原生(.parent .child
  6. 6 主题切换:只改 [data-theme='xxx'] 选择器,组件零改动
  7. 移动端position: fixed; inset: 0 + env(safe-area-inset-*)

接下来

样式系统懂了。继续阅读 14-Zustand状态管理.md — 项目里 80% 的业务逻辑都靠它。
或者直接看新主题系统详解:16-主题系统与ThemeSwitcher.md