设计系统:从设计稿到 CSS 变量
设计系统不是"把设计稿里的颜色抄一遍"。它是设计稿与代码之间的翻译层——一次定义,全局生效,改一个变量就能换肤。这篇文章,把从 Design Token 到 CSS 变量再到 Element Plus 主题覆写的完整链路理清楚。
一、为什么 17 个 Frame 需要一套设计系统
在上一篇文章里,我从 Figma 中提取出了颜色、字体、圆角、间距等设计属性。但提取只是第一步——如果直接把这些值写死在每个组件里,会发生什么?
假设主色#0073FB出现在了 17 个 Frame 的 80+ 处节点上。按"看到什么写什么"的直觉做法,你的代码里会出现几十次color: #0073FB、background: #0073FB、border-color: #0073FB。
然后有一天,设计师说:“主色调调浅一点,改成#1A8CFF。”
你打开 VS Code,全局搜索#0073FB,替换为#1A8CFF。运气好的话改了 40 处没出问题。运气不好的话,某个恰好也是#0073FB但不是主色的边框被你一起改掉了——这种 Bug 等你发现的时候,可能已经上线一周了。
这不是假设。我在之前的项目里亲身经历过一次"改主题色"的噩梦。从那以后,我给自己定了一条铁律:
任何在项目中重复出现 2 次以上的样式值,必须定义为变量。
这不是过度工程化,这是对自己的仁慈。
对于"管网模型工具"这个项目,17 个 Frame 的样式看起来眼花缭乱,但经过上一篇的去重归并后,真正的系统色只有 10 个左右,字号只有 4 个阶梯,圆角只有 4 个阶梯。这些数值不是散落的——它们构成了一套隐式的设计规范。我要做的,是把这套隐式规范"显式化"。
二、设计 Token 的五维分类
设计 Token 这个词听起来高大上,但本质上就是"给样式值起个有意义的名字"。我把这个项目涉及的 Token 分了五个维度:
2.1 颜色 Token
颜色是最容易失控的维度。17 个 Frame 里看起来有 50+ 种颜色,但实际上:
- 有些是同一个颜色在不同透明度下的表现(比如主色
#0073FB降低 10% 透明度做 hover 态) - 有些是同一个颜色在不同节点上的重复引用
- 有些是渐变叠加后的渲染色,不是源色
去重归并后,真正的系统色如下:
| Token 名称 | 色值 | 语义 | 使用场景 |
|---|---|---|---|
--color-bg-page | #C8E1FF | 页面底色 | 登录页背景、内容区底色 |
--color-bg-card | #F0F5FC | 卡片/容器背景 | 顶栏、侧栏、信息卡片 |
--color-bg-white | #FFFFFF | 纯白背景 | 弹窗、表格、表单内部 |
--color-primary | #0073FB | 主色/品牌色 | 主按钮、选中态、链接、图标高亮 |
--color-primary-hover | #005FCF | 主色悬停 | 按钮 hover、菜单 hover |
--color-primary-light | #E8F1FE | 主色浅底 | 选中行背景、标签背景 |
--color-success | #52C41A | 成功/启用 | 状态标签-启用、成功提示 |
--color-danger | #FF4D4F | 危险/删除 | 删除按钮、禁用状态 |
--color-text-primary | #333333 | 主文字色 | 标题、正文、表格内容 |
--color-text-secondary | #666666 | 次要文字 | 辅助说明、标签、描述 |
--color-text-placeholder | #999999 | 占位/禁用文字 | 输入框 placeholder、禁用态文字 |
--color-border | #D9D9D9 | 边框色 | 表格线、输入框边框、分割线 |
为什么要区分--color-text-primary和--color-text-secondary?举个例子:一个表单里,字段标签用#333333,辅助说明用#666666。如果直接写色值,后来设计师说"辅助说明再淡一点,改成#888888",你又要全局搜索。定义成变量后,只需改一处。
2.2 字体 Token
这个项目涉及三种字体:MiSans(中文)、AlibabaPuHuiTi(中文标题)、Inter(英文/数字)。但用户设备上不一定安装了这些字体,所以需要完整的降级策略:
| Token 名称 | 值 |
|---|---|
--font-family-base | "MiSans", "PingFang SC", "Microsoft YaHei", sans-serif |
--font-family-title | "AlibabaPuHuiTi", "MiSans", "PingFang SC", sans-serif |
--font-family-number | "Inter", -apple-system, "Segoe UI", sans-serif |
2.3 字号阶梯
17 个 Frame 中字号并非随意取值,而是有明显的阶梯规律:
| Token 名称 | 数值 | 使用场景 |
|---|---|---|
--font-size-xl | 26px | 登录页大标题、页面级标题 |
--font-size-lg | 18px | 模块标题、卡片标题 |
--font-size-md | 16px | 正文、表格内容、输入框文字 |
--font-size-sm | 14px | 辅助文字、标签、提示信息 |
--font-size-xs | 12px | 注释、徽标、次要元数据 |
注意,这不是"随便挑了 5 个字号"——这些值直接来自 Figma 中所有 TEXT 节点的fontSize字段统计结果。没有出现 13px、15px、17px 这样的奇数尺寸,也没有出现 20px、22px 这样不在阶梯内的值。这说明设计师遵循了一套清晰的字号规范,我只是把它翻译成了变量名。
2.4 圆角阶梯
| Token 名称 | 数值 | 使用场景 |
|---|---|---|
--radius-lg | 14px | 卡片、弹窗、大型容器 |
--radius-md | 10px | 输入框、下拉菜单、表格 |
--radius-sm | 8px | 标签、徽标、小按钮 |
--radius-xs | 6px | 微小元素(tooltip、badge) |
这四个值有一个共同点:都是偶数。这不是巧合——偶数值在 Retina 屏幕上渲染时不会出现半像素模糊。好的设计师会在这种细节上体现专业性。
2.5 间距阶梯
间距是设计中"最不起眼但最影响精致度"的维度。这个项目的间距体系是严格的 4px 基准体系:
--spacing-xs:4px;--spacing-sm:8px;--spacing-md:12px;--spacing-lg:16px;--spacing-xl:24px;--spacing-2xl:32px;--spacing-3xl:40px;--spacing-4xl:50px;你会发现:4 × 2 = 8、4 × 3 = 12、4 × 4 = 16……除50px外,所有间距都是 4 的倍数。这遵循了设计界的"4px 基准网格"原则,确保所有元素对齐在同一个不可见的网格上,视觉上整齐而不杂乱。
三、tokens.css:把 Token 写成代码
有了 Token 定义,下一步就是把它写成真正的 CSS。我创建了src/styles/tokens.css:
/* ======================================== 设计系统 · Design Tokens 基于 Figma "管网模型工具" 设计稿提取 ======================================== */:root{/* ── 颜色:背景 ── */--color-bg-page:#C8E1FF;--color-bg-card:#F0F5FC;--color-bg-white:#FFFFFF;/* ── 颜色:品牌/功能 ── */--color-primary:#0073FB;--color-primary-hover:#005FCF;--color-primary-light:#E8F1FE;--color-success:#52C41A;--color-danger:#FF4D4F;/* ── 颜色:文字 ── */--color-text-primary:#333333;--color-text-secondary:#666666;--color-text-placeholder:#999999;/* ── 颜色:边框/分割 ── */--color-border:#D9D9D9;/* ── 字体:家族 ── */--font-family-base:"MiSans","PingFang SC","Microsoft YaHei",sans-serif;--font-family-title:"AlibabaPuHuiTi","MiSans","PingFang SC",sans-serif;--font-family-number:"Inter",-apple-system,"Segoe UI",sans-serif;/* ── 字体:字号 ── */--font-size-xs:12px;--font-size-sm:14px;--font-size-md:16px;--font-size-lg:18px;--font-size-xl:26px;/* ── 字体:行高 ── */--line-height-tight:1.2;--line-height-normal:1.5;--line-height-loose:1.8;/* ── 圆角 ── */--radius-xs:6px;--radius-sm:8px;--radius-md:10px;--radius-lg:14px;/* ── 间距(4px 基准网格) ── */--spacing-xs:4px;--spacing-sm:8px;--spacing-md:12px;--spacing-lg:16px;--spacing-xl:24px;--spacing-2xl:32px;--spacing-3xl:40px;--spacing-4xl:50px;/* ── 阴影 ── */--shadow-card:0 2px 8pxrgba(0,0,0,0.08);--shadow-dialog:0 4px 16pxrgba(0,0,0,0.12);--shadow-selected:0 0 0 2pxvar(--color-primary-light);/* ── 过渡动画 ── */--transition-fast:0.15s ease;--transition-normal:0.3s ease;/* ── 布局 ── */--layout-topbar-height:120px;--layout-sidenav-width:200px;--layout-content-padding:24px;}这里有几个设计决策值得展开:
1. 阴影也定义为 Token
Figma 中卡片有0 2px 8px rgba(0,0,0,0.08)的投影,弹窗有稍重的0 4px 16px rgba(0,0,0,0.12)。如果不定义为 Token,很容易出现"这个弹窗的阴影比另一个深一点"的不一致问题。
2. 布局尺寸也纳入 Token
顶栏高度 120px、侧栏宽度 200px——这些值在多个布局组件中会反复引用。把它们定义为 Token,改布局时只需要改一个地方。
3. 过渡动画统一
所有交互(hover、展开、切换)使用相同的缓动曲线和时长。用户感知到的"流畅度"很大程度上取决于动画的一致性。
四、Element Plus 主题覆写:让组件库听话
设计 Token 定义好了,但 Element Plus 组件有自己的默认样式——蓝色主色是#409EFF,圆角是3px,和我们的设计稿完全不同。
要让 Element Plus"穿"上我们的设计系统,需要做两级覆写。
4.1 理解 Element Plus 的样式变量体系
Element Plus 2.x 使用 CSS 变量来控制主题,所有变量都以--el-开头。官方文档列出了 100+ 个可覆写的变量,但实际项目中不需要全改。核心变量就这些:
| Element Plus 变量 | 说明 |
|---|---|
--el-color-primary | 主色 |
--el-color-primary-light-3~light-9 | 主色的浅色变体 |
--el-border-radius-base | 默认圆角 |
--el-font-size-base | 默认字号 |
--el-text-color-primary | 主文字色 |
--el-text-color-regular | 常规文字色 |
--el-border-color-base | 边框色 |
4.2 用 SCSS 覆写
在src/styles/下创建element-override.scss:
/* ======================================== Element Plus 主题覆写 将 design tokens 映射到 Element Plus CSS 变量 ======================================== */ // ── 主色 ── --el-color-primary: var(--color-primary); --el-color-primary-light-3: var(--color-primary-light); --el-color-primary-light-5: var(--color-primary-light); --el-color-primary-light-7: var(--color-primary-light); --el-color-primary-light-8: var(--color-primary-light); --el-color-primary-light-9: var(--color-primary-light); --el-color-primary-dark-2: var(--color-primary-hover); // ── 文字色 ── --el-text-color-primary: var(--color-text-primary); --el-text-color-regular: var(--color-text-secondary); --el-text-color-placeholder: var(--color-text-placeholder); // ── 边框 ── --el-border-color-base: var(--color-border); --el-border-color-light: var(--color-border); // ── 圆角 ── --el-border-radius-base: var(--radius-md); --el-border-radius-small: var(--radius-sm); // ── 字号 ── --el-font-size-base: var(--font-size-md); --el-font-size-small: var(--font-size-sm); --el-font-size-extra-small: var(--font-size-xs); --el-font-size-large: var(--font-size-lg); // ── 背景 ── --el-bg-color: var(--color-bg-white); --el-bg-color-page: var(--color-bg-page);这里有一个技术细节:Element Plus 的主色会衍生出light-3到light-9九个浅色变体,用于 hover 背景、选中态背景等。如果不覆写它们,会出现"主按钮是#0073FB,但 hover 时背景变成#409EFF浅色"的诡异现象。我把它们统一映射到--color-primary-light,虽然精度上会损失一些渐变层次,但对于演示原型来说足够用了。
4.3 全局样式导入顺序
样式的导入顺序有严格要求。在main.js中:
// 1. Element Plus 默认样式(必须先加载)import'element-plus/dist/index.css'// 2. 设计 Token(定义 CSS 变量)import'@/styles/tokens.css'// 3. Element Plus 主题覆写(消费 Token 变量)import'@/styles/element-override.scss'// 4. 全局自定义样式import'@/styles/global.css'顺序错了会有问题:如果在 Token 定义之前就覆写 Element Plus,CSS 变量找不到对应的值,会回退到浏览器默认值,整个色彩体系就乱套了。
五、字体加载方案:CDN + 本地 + 回退三级策略
字体是这个设计系统里"不可见但很重要"的一环。设计师用了 MiSans(小米字体)和 AlibabaPuHuiTi(阿里普惠体),但用户的电脑上大概率没有。
我的策略分三级:
第一级:CDN 加载(最优解)
如果字体官方提供了 CDN,优先使用。CDN 的优势是:不需要打包进项目(省体积),而且用户浏览器可能已经缓存了这些字体。
/* 在 index.html 中通过 <link> 引入,或使用 @import */@importurl('https://cdn.jsdelivr.net/npm/misans@latest/lib/Normal/MiSans-Normal.min.css');第二级:本地打包(兜底方案)
如果 CDN 不可用(比如内网环境),将字体文件放入src/assets/fonts/,用@font-face声明:
@font-face{font-family:'MiSans';src:url('@/assets/fonts/MiSans-Normal.woff2')format('woff2'),url('@/assets/fonts/MiSans-Normal.woff')format('woff');font-weight:400;font-style:normal;font-display:swap;/* 先显示回退字体,加载完再切换 */}第三级:系统字体回退
无论 CDN 还是本地打包,最后的font-family声明里一定带系统字体做降级:
font-family:"MiSans","PingFang SC","Microsoft YaHei",sans-serif;这样即使用户的设备完全没有 MiSans,也能用 PingFang SC(macOS)或 Microsoft YaHei(Windows)渲染出视觉效果接近的中文。
font-display: swap也值得单独提一句:它告诉浏览器"先用回退字体渲染文字,等自定义字体下载完成后再替换"。这避免了"字体没加载完,页面白屏"的 FOIT(Flash of Invisible Text)问题。
六、设计系统的工程价值
一套设计 Token + CSS 变量 + Element Plus 覆写搭完之后,它带来的价值会随着项目推进越来越明显:
1. 改色 = 改一行的时代
设计师说"主色改成暖色系"。你打开tokens.css,改一行:
--color-primary:#FF6B35;/* 原来 #0073FB */--color-primary-hover:#E55A2B;/* 原来 #005FCF */--color-primary-light:#FFF0E8;/* 原来 #E8F1FE */刷新页面。整个项目的所有主按钮、选中态、链接、图标高亮、标签背景——从登录页到模型发布的每一个 Frame——全部变成暖色系。一秒完成。
这就是"一次定义,全局生效"的真正含义。
2. 新页面开发成本骤降
当你开发第 8 篇的"API 注册管理"页面时,你不需要再去翻 Figma 看"表格的边框是什么颜色"、“弹窗的圆角是多少”、“间距应该用 16px 还是 24px”。这一切都在 Token 里:
<style scoped> .api-table { background: var(--color-bg-white); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-xl); } .api-table .header { color: var(--color-text-primary); font-size: var(--font-size-lg); margin-bottom: var(--spacing-lg); } </style>你写的不是"这个页面怎么设计",而是"这个页面用哪个 Token"。新页面开发变成了一场"Token 拼图"——而拼图比从零画图快得多。
3. 团队协作的语言统一
如果是多人开发,设计系统还有一个隐藏价值:统一沟通语言。
设计师说"这个按钮用主色",前端直接写var(--color-primary)。不需要问"主色是哪个色值来着?",不需要对着 Figma 取色器手动吸色。设计师和前端说的是同一套 Token 语言,沟通成本直线下降。
七、一个文件搞定:给读者的"设计系统鸡尾酒"
最后,如果你不想像我这样拆成tokens.css+element-override.scss+global.css三个文件,可以把它们合并成一个theme.css直接引入。对于个人项目或小团队来说,一个文件反而更方便维护。
我把三合一版本也放在这里,你可以根据自己的项目规模选择哪种组织方式:
/* ======================================== theme.css 设计系统 + Element Plus 覆写,一站式 放置于 src/styles/theme.css 在 main.js 中 element-plus/dist/index.css 之后引入 ======================================== */:root{/* 设计 Token */--color-bg-page:#C8E1FF;--color-bg-card:#F0F5FC;--color-bg-white:#FFFFFF;--color-primary:#0073FB;--color-primary-hover:#005FCF;--color-primary-light:#E8F1FE;--color-success:#52C41A;--color-danger:#FF4D4F;--color-text-primary:#333333;--color-text-secondary:#666666;--color-text-placeholder:#999999;--color-border:#D9D9D9;--font-family-base:"MiSans","PingFang SC","Microsoft YaHei",sans-serif;--font-family-title:"AlibabaPuHuiTi","MiSans","PingFang SC",sans-serif;--font-family-number:"Inter",-apple-system,"Segoe UI",sans-serif;--font-size-xs:12px;--font-size-sm:14px;--font-size-md:16px;--font-size-lg:18px;--font-size-xl:26px;--line-height-tight:1.2;--line-height-normal:1.5;--line-height-loose:1.8;--radius-xs:6px;--radius-sm:8px;--radius-md:10px;--radius-lg:14px;--spacing-xs:4px;--spacing-sm:8px;--spacing-md:12px;--spacing-lg:16px;--spacing-xl:24px;--spacing-2xl:32px;--spacing-3xl:40px;--spacing-4xl:50px;--shadow-card:0 2px 8pxrgba(0,0,0,0.08);--shadow-dialog:0 4px 16pxrgba(0,0,0,0.12);--shadow-selected:0 0 0 2pxvar(--color-primary-light);--transition-fast:0.15s ease;--transition-normal:0.3s ease;--layout-topbar-height:120px;--layout-sidenav-width:200px;--layout-content-padding:24px;/* Element Plus 覆写 */--el-color-primary:var(--color-primary);--el-color-primary-light-3:var(--color-primary-light);--el-color-primary-light-5:var(--color-primary-light);--el-color-primary-light-7:var(--color-primary-light);--el-color-primary-light-8:var(--color-primary-light);--el-color-primary-light-9:var(--color-primary-light);--el-color-primary-dark-2:var(--color-primary-hover);--el-text-color-primary:var(--color-text-primary);--el-text-color-regular:var(--color-text-secondary);--el-text-color-placeholder:var(--color-text-placeholder);--el-border-color-base:var(--color-border);--el-border-color-light:var(--color-border);--el-border-radius-base:var(--radius-md);--el-border-radius-small:var(--radius-sm);--el-font-size-base:var(--font-size-md);--el-font-size-small:var(--font-size-sm);--el-font-size-extra-small:var(--font-size-xs);--el-font-size-large:var(--font-size-lg);--el-bg-color:var(--color-bg-white);--el-bg-color-page:var(--color-bg-page);}八、小结:设计系统是"可维护性"的入场券
如果说上一篇的"读 Figma"是从混沌中提取秩序,那这一篇的"搭设计系统"就是把那个秩序固化下来。
它不会让你的页面立刻变好看——Token 本身不是视觉效果,它只是变量的名字。但它是整个项目可维护性的基础。没有它,你每写一个颜色都要犹豫"是这个色值吗",改一个色值要担心"会不会影响其他地方"。
设计系统的价值不在"写"的那一刻,而在"改"的那一刻。
Token → CSS 变量 → Element Plus 覆写,这三层下来,你的项目就拥有了一套完整的"翻译系统":设计师的语言通过 Figma 进入 Token,Token 通过 CSS 变量进入组件,组件通过覆写进入 Element Plus。从此以后,"还原设计稿"不是一场手工作坊式的复制粘贴,而是一次工程化的映射。
上一篇:03 - Figma 数据提取:用 MCP 工具读懂设计稿
下一篇预告:设计系统就位了,下一步是什么?搭脚手架——用 Vite 5 分钟搭出标准项目结构,把 Token、路由、状态管理、Mock 数据全部各就各位。