设计令牌与styling.md:从CSS变量到跨端样式的统一管理
1. 设计令牌不是"变量命名"那点事:先搞清楚它到底解决什么
很多人一听到"设计令牌"(Design Token),第一反应是"这不就是给颜色、字号起个变量名吗"。这个理解不能算错,但停留在这一层,基本等于把一辆跑车当拖拉机开。我在实际项目里见过太多团队,花了大力气把样式抽成一堆变量,结果半年后维护成本比原来还高,最后又改回硬编码,然后得出一个结论:"设计令牌没用,是过度设计。"
真实情况是,设计令牌解决的不是"少写几行CSS"的问题,而是跨端一致性、品牌可扩展性和设计决策的单一事实来源。它是一套命名和传递设计决策的协议,让设计师、前端、客户端、甚至运营投放都能使用同一套"设计语言"沟通。
举个例子。你定义一个颜色:
这只是一个变量。但如果你这样定义:
这就是一组令牌。它不只是存了颜色值,还编码了语义层级(brand、primary、hover),让任何人在任何地方看到 --color-brand-primary-hover,就知道这是品牌主色的悬停态,不需要去查设计稿。
更关键的是,好的令牌体系会做层级拆分。我在之前参与的一个中后台项目里,设计的令牌体系分了四层:
- 基础令牌(Primitive Token):最底层的原始值,比如
gray-50: #fafafa、blue-500: #1677ff,它不关心业务,只负责表达 "这个颜色是蓝色系里的第5档"。 - 语义令牌(Semantic Token):把基础令牌映射到业务语义,比如
color-bg-page: gray-50、color-text-primary: gray-900,它表达的是"页面背景用什么颜色",而不是"页面背景是一个叫 #fafafa 的颜色"。 - 组件令牌(Component Token):在语义令牌之上,针对具体组件定义,比如
button-primary-bg: color-brand-primary。 - 品牌令牌(Brand Token):最顶层的品牌级变量,比如
brand-color: color-brand-primary,方便未来换肤或品牌升级时只改一处。
这套分层的好处在于:如果你想做暗黑模式,只需要替换语义令牌那一层;如果你想做年度改版,只需要替换品牌令牌那一层;如果只是某个组件微调,改组件令牌那一层。每一层的改动都有清晰的影响边界,不会牵一发动全身。
我当时被问得最多的一个问题是:"搞这么多层,一个颜色改起来不是要穿很多层吗?"答案是,你不该直接去改底层Primitive Token。当你需要调整品牌色时,改的应该是 brand-color 这个品牌令牌,最多再同步一下 hover、active 等衍生状态,底层的基础色板基本不动。这种设计就是为了让"日常改动"只发生在最上层,底层保持稳定。
所以,如果你正准备做设计令牌体系,第一件事不是打开代码编辑器,而是坐下来想清楚:我的项目需要几层令牌?哪些层会经常变?哪些层几乎不变? 这个想法的清晰程度,决定了后面所有工作的质量。
2. styling.md:把设计令牌变成团队协作契约
"styling.md" 这个名字,表面上看起来只是一个 Markdown 文件,但它本质上是设计令牌的文档化契约。我自己的经验是,设计令牌失败的项目里,有八成不是技术方案不行,而是"令牌和设计稿对不上"或者"团队成员根本不知道有哪些令牌可以用"。
styling.md 要解决的核心痛点有三个:
-
设计师和开发之间的"翻译损耗"。设计师在 Figma 里标注了一个颜色或者圆角,开发者在代码里要找半天对应关系。如果你的团队成员能打开 styling.md,一眼看到
--radius-md对应设计稿里的 "圆角 8px",这个损耗就没了。 -
令牌的可见性和可发现性。代码里的变量只有写了才知道存在,新来的同事经常自己重新定义一遍,结果项目里出现好几个接近但不同的灰色。styling.md 相当于一个"可搜索的令牌目录",新人进来第一件事就是读这个文件。
-
设计决策的溯源与解释。很多项目里有几十个灰色,但没人说得清为什么要有这么多。styling.md 可以在每个令牌下面写一行注释,说明"这个色值用于什么场景""和哪个设计规范对应",让每个决策都有据可查。
一个可落地的 styling.md 我建议至少包含以下结构:
2.1 文件头:版本与使用说明
这一部分看似简单,但真的很重要。没有版本号的规范文件,改的时候没有人敢动;没有维护人,改坏了没人担责,最后大家都不敢用。这个文件一旦定稿,就要走版本管理,每次改动都要更新版本号和更新记录。
2.2 命名规范:一套能读懂的命名体系
设计令牌的命名,我建议统一采用 BEM 变体格式:
比如:
--color-bg-page:页面背景色--color-text-primary:一级文字色--radius-btn-sm:按钮小尺寸圆角--shadow-card-hover:卡片悬浮阴影--space-padding-lg:大号内边距
命名里最容易犯的错是"缩写过多"。比如 --clr-txt-prim,看起来省了几个字符,但每次阅读都要解码,协作成本远高于那点字符节省带来的收益。我见过一个极端的项目,令牌名全用拼音缩写,新人来了根本猜不出 --ys-bj-zj 是什么意思,最后文档形同虚设。
另一个要点是状态命名要遵循同一套词汇表。hover、active、disabled、focus 这些状态词要固定下来,禁止混用 hover 和 mouseover、disabled 和 disable 这类近义词。建议把词汇表也放在 styling.md 里:
| 状态 | 统一用词 | 禁用词 |
|---|---|---|
| 悬停 | hover | mouseover, over |
| 激活 | active | pressed, down |
| 禁用 | disabled | disabled-state, off |
| 聚焦 | focus | focused, on-focus |
| 危险 | danger | error, wrong |
| 警告 | warning | warn, alert |
这套词汇表看起来是小事,但它能保证整个团队说的"hover"是同一个东西。没有这个约定,第三个开发者加入的时候,项目里已经有 --btn-color-hover 和 --btn-hover-color 两种写法了。
2.3 分模块记录:按使用场景分组
styling.md 不是把几百个令牌列一个大表格就完事,那样可读性会非常差。我习惯按使用场景分成几个大模块:
- 色彩系统:品牌色、功能色、中性色、文本色、边框色、背景色
- 字体系统:字号、字重、行高、字间距
- 间距系统:内边距、外边距、栅格间距
- 圆角系统:全局圆角、组件圆角
- 阴影系统:基础阴影、悬浮阴影、弹窗阴影
- 动效系统:时长、缓动函数
- 层级系统:z-index 的分层规范
每个模块下面用表格列令牌,字段包括:
比如:
把"对应设计稿"这一列加上,是为了让设计师也能用这份文档,她能直接跳到 Figma 里的对应样式,核对是否一致。这份文档就成为真正意义上"设计和开发之间的翻译器"。
2.4 使用原则:什么情况能用,什么情况不能用
这部分是很多人忽略的。没有使用原则的 styling.md,很快就会被"图省事"的开发者攻破。我通常会写清楚这几条:
- 新颜色必须从颜色系统里选,如果现有令牌没有合适的,先找设计确认,不要自己新加色值。
- 圆角优先用令牌,不要用"看起来差不多"的数字。
- 动画时长统一用令牌,禁止写
transition: all 0.3s这种魔法值。 - 字体大小、行高都要用令牌,不要在组件里单独设置 font-size。
同时,要明确例外情况的处理方式:如果需要一次性微调,可以直接用 CSS 覆盖,但要加注释说明"这是设计例外,后续要回归令牌",并且在下个迭代回收。
在团队落地的时候,这份 styling.md 最好和自动化工具关联。比如我在项目中会用 Stylelint 做规则校验,配置了"禁止在样式表里出现硬编码颜色值"的规则,凡是绕过令牌的写法直接报错。这个强制手段比任何文档都有效。
3. 从 style guide 到实际代码:令牌落地的完整链路
styling.md 写好了,接下来最关键的问题是:怎么让它真正跑到代码里,而不只是一份躺在文档库里吃灰的 Markdown。
3.1 CSS 变量:最直接的落地方式
对于 Web 项目,最直接的落地方式是把 styling.md 里的令牌定义成 CSS 自定义属性(CSS Variables)。这一步其实没什么技术含量,就是把 Markdown 里的表格转成代码:
定义在 :root 上,意味着所有元素都能访问。对于 Vue 项目,你可以放在全局样式文件里,比如 src/styles/tokens.css,在 main.ts 里引入。然后组件里这么用:
这样写的好处是,当产品经理说"品牌色往左偏一点",你只需要改 :root 里那一个值,全站按钮、链接、选中态、加载动画的颜色会同步刷新。这个效果在传统硬编码时代是完全不敢想象的。
3.2 暗黑模式的实现:令牌分层的真正威力
令牌分层的威力,在暗黑模式场景下体现得最明显。
假设你的设计令牌体系做到了语义层和基础层的分离,实现暗黑模式就变成了一个非常干净的过程:
组件代码完全不用动,因为组件里始终引用的是 var(--color-bg-page),而不是具体的色值。只需要在根元素上切换 data-theme 属性,整个系统的配色就会整体切换。
我在一个实际项目里就是这么做的。当时领导要求"三天内上线暗黑模式",我用了两个晚上把全部组件的硬编码颜色替换成语义令牌,第三天只花了一个小时就完成了暗黑模式的切换逻辑。当时团队的另一个前端觉得不可思议,其实原理一点都不复杂——一切归功于当初搭令牌体系时多花的那几步。
3.3 跨端同步:用 JSON 打通设计系统
如果你的项目不是纯 Web,还有小程序、React Native、Flutter,那 CSS 变量就不够了。这时候我推荐把 styling.md 里的令牌同步维护一份 JSON 文件,作为"CDP(Cross-Platform Design Tokens)":
然后通过脚本,把 JSON 编译成各端需要的格式:
- 给 Web 用:编译成 CSS Variables
- 给 Tailwind 用:编译成 tailwind.config.js 里的 theme 配置
- 给 React Native / Flutter 用:编译成对应的主题对象或静态常量
这种做法让"design token 一次定义,处处使用"成为现实。我以前在团队里甚至写过一个简单的 Node 脚本,监听 JSON 文件变化,自动重新生成各家平台样式文件,开发体验就是改一个 JSON,全端代码同步更新。这套流程跑顺了之后,再也没人想回到"每个端各维护一套色值"的时代。
4. 做好样式设计:不止是令牌本身,更是团队工作流的改造
很多团队把设计令牌当成一个"前端技术任务",这是最大的误区。设计令牌系统做得成功的团队,往往都把它当成一场团队工作流的重新梳理。我在推动设计令牌落地的过程中,踩了不少坑,也积累了一些可以复用的经验,挑几个重要的分享出来。
4.1 推动设计的隐藏原则:先统一"设计语言",再统一"代码变量"
如果你只是在前端这边定义令牌,设计师那边完全不知道你在干什么,那这个体系就是空中楼阁。正确顺序应该是:
- 和设计师一起盘一次现有的设计稿,统计用了多少种颜色、字号、圆角。这个工作非常关键,你会惊讶地发现一个"看上去挺统一"的产品,实际可能存在 30 多种灰色、20 多种字号。
- 根据统计结果,和设计师一起定义"最小可用集",剔除重复和近似值,收敛出核心色板、字号阶梯、间距标尺。
- 把这些收敛后的值定义成设计令牌,并在 Figma 里同步建立对应的样式变量。
- 前端再基于这份双方确认过的令牌写代码。
我见过无数团队跳过前两步,直接让前端"凭感觉"定义令牌,结果方案出来之后设计师不认,因为他们觉得色板不是他们定义的那些颜色,后续的协作就变成了一场拉锯战。
可以做得更彻底一点:让设计系统负责人(或团队里最熟悉设计的前端)和设计师坐在一起,把 Figma 的样式面板打开,逐个核对每个样式对应的令牌名和值。这个过程虽然琐碎,但一旦完成,后续很长一段时间的设计和开发都会非常顺畅。
4.2 版本管理:令牌变更要有"变更日志"
设计令牌不是一次性定义完就永远不变的。它会跟随品牌升级、界面改版、暗黑模式等需求持续演化。我建议在 styling.md 文件末尾维护一个 changelog:
这个 changelog 最大的作用是"安全和可追溯"。当有人在群里问"这个颜色什么时候改的?为什么跟设计稿不一样",你不需要翻代码提交记录,直接看 changelog 就能回答。它的存在也让团队成员敢于修改令牌,因为他们知道每一次修改都是有记录的。
4.3 常见坑:全局排障的一些思路
根据我在项目里的排障经验,样式和令牌相关的问题很多都有固定的套路可以排查,下面记录几个常见的场景和对应思路。
问题一:改了 :root 里的变量,页面没有变化。
- 检查是不是在 scoped 样式里重新定义了同名变量。Vue 的 scoped 样式会给选择器加
data-v-xxx属性,如果你在某个组件的 scoped 样式里写了:root { --color-brand-primary: red },作用域可能不符合预期。 - 检查是不是
!important覆盖。 - 检查变量引用的元素是不是在
:root定义的作用域之外(比如微前端架构中的子应用场景)。
问题二:.el-table::before 修改样式不生效。
这个是一个很经典的框架样式问题,Element Plus 的表格组件在 .el-table::before 上定义了用于占位的左线/右线的伪元素样式,它出现在表格底部。常规 CSS 可能被组件内部更高优先级的选择器覆盖。解决方案通常是:
注意 !important 在这里确实属于比较粗暴的手段,但针对第三方组件库的固定样式,这往往是成本最低的做法。更深层的原因是因为 Element Plus 在样式设计上用到了包括 ::before 在内的多种伪元素技巧,且部分版本选择器优先级较高,普通覆盖很难生效。
问题三:Tabs 标签页样式和 Dialog 组件样式修改不生效。
这类问题的根源都是组件渲染到了 body 下或使用了 Teleport 传送门。Vue 的 Dialog、Popover、Tooltip 之类的浮层组件默认挂载在 body 下面,所以你在 scoped 样式里写的 .el-dialog {} 根本不可能命中它,因为 scoped 会加 data-v-xxx 属性,而 body 下的 DOM 没有这个属性。
解决方案:如果是全局覆盖,可以专门建一个非 scoped 的样式文件,或者在 :global() 里写覆盖样式。也可以利用 CSS 变量体系来调整组件库的主题,Element Plus 3.x 已经支持通过 CSS 变量配置大部分组件的样式,我通常会维护一份 theme.css 专门放这类覆盖。
4.4 样式隔离与团队协作:让令牌真正"跑"起来
最后想说的是,设计令牌能不能成功,技术只占一半,另一半在于你有没有一套让所有人都能信任和使用它的机制。
信任的来源是"可见"和"可控"。所以我建议在项目里专门建一个"令牌预览页",把所有令牌可视化出来。这个页面可以是 Storybook 里的一个 story,也可以是一个独立路由。它的作用就是让开发者和设计师随时能看到:
- 所有颜色令牌的长什么样
- 改变换肤主题后会发生什么
- 间距、圆角这些非颜色令牌用起来是什么效果
我实际做过的做法是写一个 Vue 组件,遍历一个令牌对象,把色块渲染成网格:
有了这个页面之后,合作的效率提升非常明显。设计师可以直接指着色块说"我要这个颜色做背景",开发者不用猜是哪个变量。这个页面也成了团队新人培训的最佳材料。
5. 从样式管理到企业级系统:留给你的三个实操建议
写到这里,关于设计令牌和 styling.md 的理论和应用已经讲得比较全面了。最后我想结合个人经验,给出三条最实用的建议。如果你想在企业级系统里落地设计令牌,这三点是我目前认为优先级最高的。
建议一:先做现状盘点,不要先建新体系。
不管你觉得现有系统有多乱,先做一次完整的样式盘点。用脚本扫描项目里所有 CSS/SCSS/Vue 文件里的硬编码颜色,统计出现频次,导出前 20 种最常见的色值。你会发现品牌色、功能色、中性色哪些被用多了,哪些其实根本不需要。基于这个盘点结果,你才知道令牌体系里最核心的几个颜色是什么,而不是拍脑门定义一堆用不上的令牌。
建议二:令牌优先,强制校验比文档更有效。
文档写得再详细,也拦不住开发赶时间时随手写一个 #cccccc。最有效的落地方式是把"硬编码色值"变成编译错误或者 CI 校验错误。我在项目里用 Stylelint 做规则限制,关键配置大概长这样:
加上这个限制之后,一开始大家确实有些不适应,因为太习惯了直接写颜色。但坚持两三个迭代之后,组件库的颜色使用就变得非常规范,新组件也下意识地找令牌。
建议三:在设计系统早期就考虑"换肤"需求。
大多数业务系统早晚会遇到"客户要定制品牌色"的需求。提前把品牌令牌单独抽出来,比如 --brand-primary、--brand-primary-hover、--brand-bg 这几个,将来客户要求改色的时候,后端下发一个主题配置,前端动态改变几个 CSS 变量的值,整个系统的品牌色就能切换。如果等到项目后期再去做这件事,那绝对是一场灾难。
从我个人的经验来看,设计令牌这套方法的全称其实是"一次思考,处处复用"。它前期会多花一些时间和设计师对稿、梳理语义、拆分层级,但一旦跑通,后面开发新页面、支持暗黑模式、对接多个客户端,效率提升都是肉眼可见的。如果你所在的项目还在用硬编码样式堆页面,我强烈建议你从这篇 styling.md 开始,把"先定义、后使用"的工作习惯带进团队里。