3、设计系统基础:设计Token的概念、颜色与字体规范、间距与阴影体系、设计Token的实现

好,咱们进入正题。这一章聊的是设计系统的地基——设计Token。说白了,Token就是设计语言里的「原子变量」。你想想看,一个按钮的颜色、一段文字的间距、一个卡片的阴影,如果每个地方都硬编码,那改起来就是灾难。

我在项目中遇到过最典型的例子:一个产品有3个品牌,每个品牌的主色不同。如果没有Token,你得全局搜索#1890ff然后手动替换。有了Token,改一个变量值,全站跟着变。这就是Token的价值。

3.1 什么是设计Token

设计Token,本质上就是「设计属性的抽象命名」。它把具体的数值(比如16px、#333)包装成一个有语义的名字(比如spacing-mdcolor-text-primary)。

我个人习惯把Token分成三个层级:

  • 基础Token:最底层的原子值,比如color-blue-500: #1890fffont-size-16: 16px
  • 语义Token:基于基础Token的语义化映射,比如color-primary: color-blue-500font-body: font-size-16
  • 组件Token:具体到组件的变量,比如button-bg: color-primarycard-padding: spacing-md

核心原则:基础Token只存原始值,语义Token做映射,组件Token做最终消费。这样改一个基础值,所有层级自动更新。

3.2 颜色与字体规范

颜色这块,我踩过不少坑。早期我直接写#1890ff#f5222d,后来发现设计师给的色板有10个灰度、6个主色、4个功能色。如果每个都硬编码,维护成本极高。

我的做法是:先定义色板,再定义语义色。

色板定义

// 基础色板
--color-blue-50: #e6f7ff;
--color-blue-100: #bae7ff;
--color-blue-200: #91d5ff;
--color-blue-300: #69c0ff;
--color-blue-400: #40a9ff;
--color-blue-500: #1890ff;
--color-blue-600: #096dd9;
--color-blue-700: #0050b3;
--color-blue-800: #003a8c;
--color-blue-900: #002766;

// 中性色
--color-gray-50: #fafafa;
--color-gray-100: #f5f5f5;
--color-gray-200: #e8e8e8;
--color-gray-300: #d9d9d9;
--color-gray-400: #bfbfbf;
--color-gray-500: #8c8c8c;
--color-gray-600: #595959;
--color-gray-700: #434343;
--color-gray-800: #262626;
--color-gray-900: #1f1f1f;

语义色映射

// 语义色
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-400);
--color-primary-active: var(--color-blue-600);
--color-success: #52c41a;
--color-warning: #faad14;
--color-error: #f5222d;
--color-text-primary: var(--color-gray-800);
--color-text-secondary: var(--color-gray-500);
--color-bg-primary: #ffffff;
--color-bg-secondary: var(--color-gray-50);

我的经验:语义色不要超过12个。超过这个数,设计师和开发都会晕。我曾经维护过一个有30个语义色的系统,最后连设计师自己都分不清color-info-lightcolor-info-bg的区别。

字体规范

字体Token相对简单,但容易忽略「行高」和「字重」的配合。我建议把字体相关的Token统一管理:

// 字体族
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-family-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;

// 字号
--font-size-xs: 12px;
--font-size-sm: 14px;
--font-size-md: 16px;
--font-size-lg: 20px;
--font-size-xl: 24px;
--font-size-2xl: 32px;

// 行高
--line-height-xs: 1.4;
--line-height-sm: 1.5;
--line-height-md: 1.6;
--line-height-lg: 1.8;

// 字重
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;

注意:字号不要用奇数。12、14、16、20、24、32这些偶数在渲染时更清晰。我曾经用过13px,结果在部分浏览器上出现了模糊的渲染问题。

3.3 间距与阴影体系

间距体系,说白了就是「步进值」。我习惯用4px作为基础步进。为什么是4?因为大多数屏幕的像素是整数倍,4px能整除8、12、16、20、24,适配性最好。

间距Token

--spacing-xxs: 4px;
--spacing-xs: 8px;
--spacing-sm: 12px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;

嗯,这里要注意:间距Token不要用奇数。4、8、12、16、24、32、48、64,这些值在网格系统中能完美对齐。我见过有人用--spacing-sm: 10px,结果和8px的网格对不齐,整个页面看起来「差一点」。

阴影体系

阴影Token我一般分三个层级:低、中、高。每个层级对应不同的视觉深度。

// 阴影
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);

避坑指南:阴影的透明度不要超过0.15。我曾经为了「突出」某个卡片,用了0.3的透明度,结果在深色模式下阴影变成了「黑块」,特别难看。保持透明度在0.06~0.12之间最安全。

3.4 设计Token的实现

终于到了代码环节。Token的实现方式有很多种,我个人最推荐CSS自定义属性(CSS Variables)配合JavaScript变量。这样既能运行时修改,又能静态编译。

CSS实现

:root {
  // 基础色板
  --color-blue-500: #1890ff;
  // ... 其他基础色

  // 语义色
  --color-primary: var(--color-blue-500);
  --color-text-primary: #262626;

  // 间距
  --spacing-md: 16px;

  // 阴影
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
}

// 使用
.button {
  background-color: var(--color-primary);
  padding: var(--spacing-md);
  box-shadow: var(--shadow-sm);
}

JavaScript实现(主题切换)

// token定义
const tokens = {
  light: {
    '--color-primary': '#1890ff',
    '--color-bg-primary': '#ffffff',
    '--color-text-primary': '#262626',
  },
  dark: {
    '--color-primary': '#177ddc',
    '--color-bg-primary': '#141414',
    '--color-text-primary': '#e8e8e8',
  }
};

// 切换主题
function applyTheme(themeName) {
  const theme = tokens[themeName];
  const root = document.documentElement;
  Object.entries(theme).forEach(([key, value]) => {
    root.style.setProperty(key, value);
  });
}

// 调用
applyTheme('dark');

关键点:CSS Variables的兼容性很好(IE11除外),而且支持运行时修改。配合JavaScript,可以实现无闪烁的主题切换。我在一个百万级DAU的产品中用过这套方案,性能完全没问题。

设计Token的命名规范

命名规范我建议统一用--category-property-modifier的格式:

分类 命名示例 说明
颜色 --color-primary 主色
字体 --font-size-md 中号字号
间距 --spacing-lg 大间距
阴影 --shadow-sm 小阴影
圆角 --radius-md 中号圆角
边框 --border-width-thin 细边框

警告:不要用--color-1--spacing-a这种无意义的命名。我见过一个项目,Token命名全是--c1--c2--s1--s2,三个月后连作者自己都看不懂哪个是主色、哪个是背景色。

好了,这一章的内容就到这里。设计Token看似简单,但它是整个组件库的基石。颜色、字体、间距、阴影,每一个细节都决定了最终产品的视觉一致性。下一章我们会聊「组件设计原则」,到时候我会分享更多实战中的踩坑经验。