如何用设计 Token 统一前后台风格
【这是一篇写在此项目开发期的文章】 一个博客项目同时有 Nuxt 前台与 Vue 后台,如何避免两套前端各写各的样式?我将在这篇文章讲讲如何将「设计 Token」抽成唯一事实来源、用三层架构统一前后台,并选择落地极简毛玻璃风格的完整过程。

如何用设计 Token 统一前后台
主要问题:项目中的两套前端风格难以统一
我的博客最开始就有两个前端:面向读者的博客前台(后来迁到 Nuxt 3)和面向自己的管理后台(Vue 3 + Element Plus)。它们写出来的时间不同,这也导致二者风格在一开始就有所差异。
最典型的矛盾是颜色和圆角:
- 前台写的链接色是
#475569,后台 Element Plus 默认主题跑起来却是蓝紫色; - 前台卡片圆角 12px,后台同一块交互又变回 8px;
- 想让后台也"看起来像自己"时,只能手动去后台 CSS 里搜出一个个 HEX 值照着抄——然后抄多了就漏,漏了就歪。
更麻烦的地方是在毛玻璃风格。后台想用半透明背景 + 模糊,前台也想要,于是两处各自写了 backdrop-filter: blur(12px),到想统一调整时,得同时改两个工程里的好几处。
所以当时遇到的问题就是在统一二者的视觉风格时,需要额外做很多细琐的工作,并且任何视觉决策都只能靠记忆。
解决方案:把设计决策集中成三段式 Token
我给这套体系起名「设计 Token」(Design Tokens),并定了原则:禁止在组件里硬编码色值和尺寸。所有决策收纳进一份文档和三段式的变量文件。
第一层:原始 Token(primitive)
原始 Token 是最底层原子值,不带任何语义,算是这个项目可用的素材库。前台在 abstracts/_variables.scss:
// 中性色灰度
$color-gray-500: #adb5bd;
$color-gray-600: #868e96;
// 语义色
$color-success: #52c41a;
$color-warning: #faad14;
// 间距 — 4px 网格
$spacing-unit: 0.25rem; // 4px 基础单位
$spacing-1: $spacing-unit;
$spacing-2: $spacing-unit * 2;
// 圆角
$border-radius-sm: 0.25rem; // 4px
$border-radius-md: 0.75rem; // 12px
这一层的价值主要是可复用、可枚举。到时候写的时候可以不用 border-radius: 12px,而是用 $border-radius-md 来代替。
第二层:语义 Token(semantic)
语义 Token 才真正对应设计意图——它用来规范"主色、危险色、卡片背景分别是什么"。这一层是 CSS 变量,由主题所驱动(亮色 :root、暗色 html.dark),是运行时真正会被读取的:
:root {
--bg-card: rgba(255, 255, 255, 0.8);
--text-primary: #1e293b;
--text-secondary: #475569;
--border-color: rgba(203, 213, 225, 0.7);
--color-accent: #475569; /* 主品牌色:链接、强调、激活态 */
--color-accent-deep: #1e293b; /* 深强调:标题、重按钮 */
--shadow-card: 0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04);
}
组件永远只用语义层。这样切换亮暗主题、或整体换一个主色,只需改这一层,不用动任何一个组件。
第三层:组件级(component)
组件层是"语义 Token 的具体组合",由 Sass placeholder / mixin 承载。比如毛玻璃卡片,它把背景、模糊、边框、圆角四个语义 Token 拼成一个可复用类:
.glass-card {
background: var(--bg-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: $border-radius-lg;
}
编译期 Sass + 运行时 CSS 变量,两层分工
很多人可能会纠结"到底用 Sass 变量还是 CSS 变量"。而我则是打算都用,并且二者各管一段。
- 原始 Token(颜色、间距、字号、圆角、断点、z-index)用 Sass,因为它们是"素材",编译期就能定死,不需要在浏览器里换来换去,用 Sass 还能做
color.adjust()这类计算。 - 语义 Token(背景、文字、边框、阴影、主题色)用 CSS 变量,因为它们是随主题变化的,必须运行时读取,才能实现
html.dark一键切换,也才能让后台配置驱动的主色注入进来。
入口 main.scss 用 @use 明确分层,顺序就是依赖顺序:
// 1. 抽象层(不输出 CSS)
@use "abstracts/variables" as *;
@use "abstracts/mixins" as *;
@use "abstracts/functions" as *;
@use "abstracts/placeholders" as *;
// 2. 主题变量(输出 CSS 变量)
@use "themes" as *;
关键在于abstracts/variables 里的原始 Token 是纯 Sass 值、不生成任何 CSS;themes 才把语义层输出成 :root / html.dark 的 CSS 变量。编译出来既无冗余,又能运行时调主题。
毛玻璃风格
毛玻璃(Glassmorphism)是我目前为博客选择的视觉风格,也是最容易"做成花哨却难用"的一环。落实到 Token 上,前台只暴露三个变量:
$glass-background-light: rgba(255, 255, 255, 0.8);
$glass-background-dark: rgba(26, 26, 46, 0.65);
$glass-border-light: rgba(229, 229, 229, 0.85);
$glass-border-dark: rgba(42, 58, 92, 0.85);
$glass-blur: 12px; // 当时用的是高模糊
对应的运行时变量(亮色):
--glass-blur: 12px;
--glass-border: rgba(255, 255, 255, 0.5);
我的思路是高透明 + 高亮玻璃边 + 主题化阴影:
- 透明背景是主视觉。亮色
rgba(255,255,255,0.8)、暗色rgba(26,26,46,0.65),能够让背景图/内容透过来。 - 柔和高光玻璃边(
--glass-border)勾勒轮廓,能够让卡片从背景中区分出来。 - 主题化阴影分层(
--shadow-card/--shadow-elevated)提供景深,暗色下阴影加深以维持层次。
而 backdrop-filter 只用在卡片、导航这类独立层上,整页背景禁用——因为模糊整个页面会让滚动和渲染都变卡。
毛玻璃在移动端的问题
在最初的设计中,我发现移动端设备性能较弱,而手指一滑就要实时模糊大片区域,体验会明显掉帧。所以我最终决定移动端(≤768px)对大面积玻璃进一步降级为纯色、去掉模糊,只保留透明度和高光边维持层次。
毛玻璃在后台的问题
我当时也将毛玻璃风格应用到了后台,但是最终选择了弃用。主要是有以下几点原因:
- 后台充斥着高密度的表格、表单、状态色,需要稳定、扎实的背景做支撑。半透明会让文字跟下面的内容打架,极大地影响可读性。
- 后台一次要渲染大量表格和数据,大面积
backdrop-filter的代价远比前台高,一上来就容易掉帧。 - 高模糊
blur(12px+)在设备上又费性能、又会糊出噪点;当背景是渐变装饰图时,卡片会看起来"蒙了一层灰",文字对比度被进一步拉低——这在表格密集的后台尤其明显。
结论:只在前台保留毛玻璃
走过这一圈,我最终给毛玻璃的定位是:毛玻璃是面向读者的"前台视觉语言",不适合后台这种高密度工具型界面。 所以后台弃用了大面积毛玻璃,只在 header、侧边栏这类局部用半透明点缀;同时保留 token 命名,方便日后整体换肤时不至于完全脱离体系。
但是在使用毛玻璃时我又遇到了新的一些问题,比如说它会影响到博客背景图的展示、读者对文字内容的阅读等,接下来我会研究一下看看有没有更好的解决方案。
前后台怎么共享一套体系
聊完玻璃的取舍与问题,让我们回到体系本身:前后台命名体系和三段的骨头是同构的——后台同样有整套原始 Token、语义 Token、组件级复用,只是语义层取值更偏向"实底"。
这样的做法主要有两点好处:
- 降低沟通成本:前后台提到某个东西,说"
--bg-card"就行,不用解释是哪个 HEX。 - 单点演进:想全局加一档圆角、改主色,可以直接去 Token 加/改,而不是去三个文件里找散落的属性。



