13 — CSS Modules 与项目样式
10 分钟理解项目里所有 CSS 的写法。
包括:CSS Modules 工作机制、设计令牌系统、6 主题切换原理、常用模式。
1. CSS Modules 是什么?
CSS Modules 是一种文件命名约定,文件名包含 .module.css 就会被 Vite 处理。
1 2 3
| .item { ... } .active { ... }
|
核心价值:
- ✅ 类名永远不会冲突
- ✅ 每个组件样式独立管理
- ✅ 不需要 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.css(6 主题分组)
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; }
: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', ...;
--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
| :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); }
[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); }
|
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
| .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.ts 的 THEMES 数组加一行:
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
| *, *::before, *::after { box-sizing: border-box; }
html, body, #root { position: fixed; inset: 0; overflow: hidden; }
body { font-family: var(--font-sans); font-size: var(--text-base); color: var(--text-primary); background: var(--bg-base); overscroll-behavior: none; -webkit-overflow-scrolling: touch; }
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: var(--border-default); }
.bg-blob { position: fixed; border-radius: 50%; filter: blur(180px); } [data-theme='ink'] .bg-blob-1 { background: var(--color-primary-glow); opacity: 0.15; }
html { --sat: env(safe-area-inset-top); --sab: env(safe-area-inset-bottom); }
::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
| <div className={`${styles.item} ${active ? styles.active : ''}`}>
<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); }
.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); }
|
5.5 响应式断点
1 2 3 4 5 6 7 8 9 10 11 12
| .sidebar { position: relative; width: var(--sidebar-width); }
@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
| :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
| .sendBtn { width: 48px; height: 48px; }
|
场景 3:改侧边栏宽度
1 2
| --sidebar-width: 320px;
|
场景 4:加新主题
1 2 3 4 5 6 7 8 9 10 11
| [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
| { id: 'sunset', name: '落日橘', description: '...', swatch: '#fef3e2', mode: 'light' },
|
UI 自动显示新主题。
8. 怎么读懂别人的 CSS Module
读 CSS 的顺序:
- 看类名(命名一般能猜出作用)
- 看 display / position(布局方式)
- 看 padding / margin / gap(间距)
- 看 background / color / border(外观)
- 看 :hover / :focus / @keyframes(交互)
- 看 @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. 一段话总结
项目样式系统:
- 设计令牌(
tokens.css)按主题分组,CSS 变量统一管理 - CSS Modules 隔离每个组件样式,类名自动哈希
- 全局样式(
global.css)只放真正全局的东西 - 命名用语义(
.item、.title、.deleteBtn) - 嵌套用 CSS 原生(
.parent .child) - 6 主题切换:只改
[data-theme='xxx'] 选择器,组件零改动 - 移动端:
position: fixed; inset: 0 + env(safe-area-inset-*)
接下来
样式系统懂了。继续阅读 14-Zustand状态管理.md — 项目里 80% 的业务逻辑都靠它。
或者直接看新主题系统详解:16-主题系统与ThemeSwitcher.md。