Design Tokens 设计与管理完全指南
什么是 Design Tokens
Design Tokens(设计令牌)是设计系统的最小单位,它们是存储视觉设计属性(如颜色、字体、间距)的命名变量。通过将设计决策抽象为令牌,可以在设计工具和代码之间保持一致性。
Design Tokens 的本质:
传统方式:
设计稿 代码
─────────────────────────────
蓝色按钮 → color: #3b82f6;
16px 字体 → font-size: 16px;
8px 间距 → padding: 8px;
问题:设计变更需要手动同步所有代码
Design Tokens 方式:
设计工具 代码
↓ ↓
tokens.json → CSS 变量 / JS 常量
↓ ↓
Figma 变量 组件使用 tokens
┌─────────────────────────────────────────────────────────────┐
│ tokens.json │
├─────────────────────────────────────────────────────────────┤
│ { │
│ "color": { │
│ "primary": { "value": "#3b82f6" } │
│ }, │
│ "fontSize": { │
│ "base": { "value": "16px" } │
│ }, │
│ "spacing": { │
│ "sm": { "value": "8px" } │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────┼─────────────┐
↓ ↓ ↓
CSS 变量 Tailwind JavaScript
--color- colors. colors.
primary primary primary
Design Tokens 的价值
Design Tokens 带来的收益:
┌─────────────────┬────────────────────────────────────────────┐
│ 收益 │ 说明 │
├─────────────────┼────────────────────────────────────────────┤
│ 一致性 │ 设计和代码使用相同的值 │
│ 可维护性 │ 修改一处,全局生效 │
│ 可扩展性 │ 轻松支持多主题、多品牌 │
│ 协作效率 │ 设计师和开发者使用统一语言 │
│ 自动化 │ 自动生成代码,减少手动同步 │
│ 文档化 │ Tokens 本身就是设计规范文档 │
└─────────────────┴────────────────────────────────────────────┘
Token 分层架构
现代 Design Token 系统通常采用三层架构:
Token 三层架构:
┌─────────────────────────────────────────────────────────────┐
│ Semantic Tokens (语义层) │
│ 表达设计意图 │
├─────────────────────────────────────────────────────────────┤
│ --color-text-primary → 主要文本颜色 │
│ --color-background-surface → 表面背景色 │
│ --color-interactive-primary → 主要交互色 │
│ --spacing-component-gap → 组件间距 │
│ ↑ │
└────────────────────┼────────────────────────────────────────┘
│ 引用
┌────────────────────┼────────────────────────────────────────┐
│ ↓ │
│ Alias Tokens (别名层) │
│ 创建映射关系 │
├─────────────────────────────────────────────────────────────┤
│ --color-blue-600 → 品牌蓝色 │
│ --color-gray-100 → 浅灰背景 │
│ --spacing-4 → 16px │
│ ↑ │
└────────────────────┼────────────────────────────────────────┘
│ 引用
┌────────────────────┼────────────────────────────────────────┐
│ ↓ │
│ Primitive Tokens (基础层) │
│ 原始值定义 │
├─────────────────────────────────────────────────────────────┤
│ #3b82f6 → 具体的蓝色值 │
│ #f3f4f6 → 具体的灰色值 │
│ 16 → 像素数值 │
└─────────────────────────────────────────────────────────────┘
Token 定义示例
// tokens/primitives.json - 基础层
{
"color": {
"blue": {
"50": { "value": "#eff6ff" },
"100": { "value": "#dbeafe" },
"200": { "value": "#bfdbfe" },
"300": { "value": "#93c5fd" },
"400": { "value": "#60a5fa" },
"500": { "value": "#3b82f6" },
"600": { "value": "#2563eb" },
"700": { "value": "#1d4ed8" },
"800": { "value": "#1e40af" },
"900": { "value": "#1e3a8a" }
},
"gray": {
"50": { "value": "#f9fafb" },
"100": { "value": "#f3f4f6" },
"200": { "value": "#e5e7eb" },
"300": { "value": "#d1d5db" },
"400": { "value": "#9ca3af" },
"500": { "value": "#6b7280" },
"600": { "value": "#4b5563" },
"700": { "value": "#374151" },
"800": { "value": "#1f2937" },
"900": { "value": "#111827" }
}
},
"spacing": {
"0": { "value": "0" },
"1": { "value": "4px" },
"2": { "value": "8px" },
"3": { "value": "12px" },
"4": { "value": "16px" },
"5": { "value": "20px" },
"6": { "value": "24px" },
"8": { "value": "32px" },
"10": { "value": "40px" },
"12": { "value": "48px" },
"16": { "value": "64px" }
},
"fontSize": {
"xs": { "value": "12px" },
"sm": { "value": "14px" },
"base": { "value": "16px" },
"lg": { "value": "18px" },
"xl": { "value": "20px" },
"2xl": { "value": "24px" },
"3xl": { "value": "30px" },
"4xl": { "value": "36px" }
},
"fontWeight": {
"normal": { "value": "400" },
"medium": { "value": "500" },
"semibold": { "value": "600" },
"bold": { "value": "700" }
},
"borderRadius": {
"none": { "value": "0" },
"sm": { "value": "4px" },
"md": { "value": "6px" },
"lg": { "value": "8px" },
"xl": { "value": "12px" },
"full": { "value": "9999px" }
}
}
// tokens/semantic.json - 语义层
{
"color": {
"text": {
"primary": {
"value": "{color.gray.900}",
"description": "主要文本颜色"
},
"secondary": {
"value": "{color.gray.600}",
"description": "次要文本颜色"
},
"disabled": {
"value": "{color.gray.400}",
"description": "禁用状态文本"
},
"inverse": {
"value": "{color.white}",
"description": "反色文本(深色背景上)"
}
},
"background": {
"primary": {
"value": "{color.white}",
"description": "主背景色"
},
"secondary": {
"value": "{color.gray.50}",
"description": "次要背景色"
},
"tertiary": {
"value": "{color.gray.100}",
"description": "第三级背景色"
}
},
"border": {
"default": {
"value": "{color.gray.200}",
"description": "默认边框颜色"
},
"hover": {
"value": "{color.gray.300}",
"description": "悬停边框颜色"
},
"focus": {
"value": "{color.blue.500}",
"description": "聚焦边框颜色"
}
},
"interactive": {
"primary": {
"value": "{color.blue.600}",
"description": "主要交互色"
},
"primaryHover": {
"value": "{color.blue.700}",
"description": "主要交互色悬停"
},
"primaryActive": {
"value": "{color.blue.800}",
"description": "主要交互色按下"
}
},
"feedback": {
"success": { "value": "{color.green.600}" },
"warning": { "value": "{color.yellow.600}" },
"error": { "value": "{color.red.600}" },
"info": { "value": "{color.blue.600}" }
}
},
"spacing": {
"component": {
"paddingXs": { "value": "{spacing.2}" },
"paddingSm": { "value": "{spacing.3}" },
"paddingMd": { "value": "{spacing.4}" },
"paddingLg": { "value": "{spacing.6}" },
"gap": { "value": "{spacing.4}" }
},
"layout": {
"sectionGap": { "value": "{spacing.16}" },
"containerPadding": { "value": "{spacing.6}" }
}
}
}
Token 工具链
Style Dictionary 配置
Style Dictionary 是最流行的 Token 转换工具:
// style-dictionary.config.js
const StyleDictionary = require('style-dictionary');
// 自定义格式:CSS 变量
StyleDictionary.registerFormat({
name: 'css/custom-variables',
formatter: function({ dictionary }) {
return `:root {\n${dictionary.allTokens
.map(token => ` --${token.name}: ${token.value};`)
.join('\n')}\n}`;
}
});
// 自定义转换:token 名称格式
StyleDictionary.registerTransform({
name: 'name/kebab',
type: 'name',
transformer: function(token) {
return token.path.join('-').toLowerCase();
}
});
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
// CSS 变量输出
css: {
transformGroup: 'css',
buildPath: 'build/css/',
files: [{
destination: 'variables.css',
format: 'css/variables',
options: {
outputReferences: true // 保留引用关系
}
}]
},
// SCSS 变量输出
scss: {
transformGroup: 'scss',
buildPath: 'build/scss/',
files: [{
destination: '_variables.scss',
format: 'scss/variables'
}]
},
// JavaScript 输出
js: {
transformGroup: 'js',
buildPath: 'build/js/',
files: [{
destination: 'tokens.js',
format: 'javascript/es6'
}, {
destination: 'tokens.d.ts',
format: 'typescript/es6-declarations'
}]
},
// Tailwind 配置输出
tailwind: {
transformGroup: 'js',
buildPath: 'build/tailwind/',
files: [{
destination: 'tailwind.config.js',
format: 'javascript/tailwind'
}]
}
}
};
自定义 Tailwind 格式
// 注册 Tailwind 配置格式
StyleDictionary.registerFormat({
name: 'javascript/tailwind',
formatter: function({ dictionary }) {
const colors = {};
const spacing = {};
const fontSize = {};
dictionary.allTokens.forEach(token => {
const category = token.path[0];
const name = token.path.slice(1).join('-');
switch (category) {
case 'color':
setNestedValue(colors, token.path.slice(1), token.value);
break;
case 'spacing':
spacing[name] = token.value;
break;
case 'fontSize':
fontSize[name] = token.value;
break;
}
});
return `module.exports = {
theme: {
extend: {
colors: ${JSON.stringify(colors, null, 2)},
spacing: ${JSON.stringify(spacing, null, 2)},
fontSize: ${JSON.stringify(fontSize, null, 2)}
}
}
};`;
}
});
function setNestedValue(obj, path, value) {
let current = obj;
for (let i = 0; i < path.length - 1; i++) {
if (!current[path[i]]) {
current[path[i]] = {};
}
current = current[path[i]];
}
current[path[path.length - 1]] = value;
}
构建脚本
// package.json
{
"scripts": {
"tokens:build": "style-dictionary build",
"tokens:watch": "nodemon --watch tokens -e json --exec 'npm run tokens:build'",
"tokens:clean": "rm -rf build/"
}
}
主题系统实现
多主题 Token 定义
// tokens/themes/light.json
{
"color": {
"text": {
"primary": { "value": "{color.gray.900}" },
"secondary": { "value": "{color.gray.600}" }
},
"background": {
"primary": { "value": "{color.white}" },
"secondary": { "value": "{color.gray.50}" }
},
"surface": {
"default": { "value": "{color.white}" },
"elevated": { "value": "{color.white}" }
}
}
}
// tokens/themes/dark.json
{
"color": {
"text": {
"primary": { "value": "{color.gray.100}" },
"secondary": { "value": "{color.gray.400}" }
},
"background": {
"primary": { "value": "{color.gray.900}" },
"secondary": { "value": "{color.gray.800}" }
},
"surface": {
"default": { "value": "{color.gray.800}" },
"elevated": { "value": "{color.gray.700}" }
}
}
}
生成主题 CSS
// style-dictionary.config.js
module.exports = {
source: ['tokens/primitives.json', 'tokens/semantic.json'],
platforms: {
// 浅色主题
'css-light': {
source: ['tokens/themes/light.json'],
transformGroup: 'css',
buildPath: 'build/css/',
files: [{
destination: 'theme-light.css',
format: 'css/variables',
options: {
selector: ':root, [data-theme="light"]'
}
}]
},
// 深色主题
'css-dark': {
source: ['tokens/themes/dark.json'],
transformGroup: 'css',
buildPath: 'build/css/',
files: [{
destination: 'theme-dark.css',
format: 'css/variables',
options: {
selector: '[data-theme="dark"]'
}
}]
}
}
};
主题切换实现
// composables/useTheme.ts
type Theme = 'light' | 'dark' | 'system';
export function useTheme() {
const theme = useState<Theme>('theme', () => 'system');
const resolvedTheme = computed(() => {
if (theme.value === 'system') {
return getSystemTheme();
}
return theme.value;
});
function getSystemTheme(): 'light' | 'dark' {
if (process.server) return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
function setTheme(newTheme: Theme) {
theme.value = newTheme;
if (process.client) {
// 保存到 localStorage
localStorage.setItem('theme', newTheme);
// 应用主题
applyTheme(newTheme === 'system' ? getSystemTheme() : newTheme);
}
}
function applyTheme(resolvedTheme: 'light' | 'dark') {
document.documentElement.setAttribute('data-theme', resolvedTheme);
// 更新 meta theme-color
const themeColor = resolvedTheme === 'dark' ? '#111827' : '#ffffff';
document.querySelector('meta[name="theme-color"]')
?.setAttribute('content', themeColor);
}
// 监听系统主题变化
onMounted(() => {
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setTheme(savedTheme);
} else {
applyTheme(getSystemTheme());
}
// 监听系统主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => {
if (theme.value === 'system') {
applyTheme(getSystemTheme());
}
};
mediaQuery.addEventListener('change', handler);
onUnmounted(() => {
mediaQuery.removeEventListener('change', handler);
});
});
return {
theme,
resolvedTheme,
setTheme,
toggleTheme: () => setTheme(resolvedTheme.value === 'light' ? 'dark' : 'light')
};
}
主题切换组件
<!-- components/ThemeToggle.vue -->
<script setup lang="ts">
const { theme, resolvedTheme, setTheme } = useTheme();
const themes = [
{ value: 'light', label: '浅色', icon: '☀️' },
{ value: 'dark', label: '深色', icon: '🌙' },
{ value: 'system', label: '系统', icon: '💻' }
];
</script>
<template>
<div class="theme-toggle">
<button
v-for="t in themes"
:key="t.value"
:class="['theme-btn', { active: theme === t.value }]"
@click="setTheme(t.value)"
:aria-label="`切换到${t.label}模式`"
>
<span class="icon">{{ t.icon }}</span>
<span class="label">{{ t.label }}</span>
</button>
</div>
</template>
<style scoped>
.theme-toggle {
display: flex;
gap: var(--spacing-2);
padding: var(--spacing-1);
background: var(--color-background-secondary);
border-radius: var(--border-radius-lg);
}
.theme-btn {
display: flex;
align-items: center;
gap: var(--spacing-2);
padding: var(--spacing-2) var(--spacing-3);
border: none;
background: transparent;
border-radius: var(--border-radius-md);
cursor: pointer;
transition: background 0.2s;
}
.theme-btn:hover {
background: var(--color-background-tertiary);
}
.theme-btn.active {
background: var(--color-background-primary);
box-shadow: var(--shadow-sm);
}
.label {
color: var(--color-text-primary);
font-size: var(--font-size-sm);
}
</style>
在组件中使用 Tokens
CSS 变量方式
<!-- components/Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot />
</div>
</div>
</template>
<style scoped>
.card {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.card-header {
padding: var(--spacing-component-padding-md);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-background-secondary);
}
.card-body {
padding: var(--spacing-component-padding-md);
}
</style>
TypeScript 类型支持
// types/tokens.ts
// 由 Style Dictionary 自动生成
export interface DesignTokens {
color: {
text: {
primary: string;
secondary: string;
disabled: string;
};
background: {
primary: string;
secondary: string;
tertiary: string;
};
interactive: {
primary: string;
primaryHover: string;
primaryActive: string;
};
};
spacing: {
component: {
paddingXs: string;
paddingSm: string;
paddingMd: string;
paddingLg: string;
};
};
// ... 更多类型
}
// 使用
import type { DesignTokens } from '@/types/tokens';
import tokens from '@/build/js/tokens';
const primaryColor = (tokens as DesignTokens).color.interactive.primary;
结合 Tailwind CSS
// tailwind.config.js
const tokens = require('./build/js/tokens');
module.exports = {
theme: {
extend: {
colors: {
// 使用 CSS 变量实现主题切换
text: {
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)'
},
bg: {
primary: 'var(--color-background-primary)',
secondary: 'var(--color-background-secondary)'
},
// 或直接使用 token 值(静态)
brand: tokens.color.blue
},
spacing: tokens.spacing,
fontSize: tokens.fontSize,
borderRadius: tokens.borderRadius
}
}
};
<template>
<!-- 使用 Tailwind 类名 -->
<div class="bg-bg-primary text-text-primary p-4 rounded-lg">
<h2 class="text-xl font-semibold text-text-primary">
标题
</h2>
<p class="text-text-secondary">
描述内容
</p>
</div>
</template>
Token 文档生成
自动生成文档
// scripts/generate-docs.js
const tokens = require('./build/js/tokens');
const fs = require('fs');
function generateColorSwatches(colors, prefix = '') {
let html = '<div class="color-grid">';
for (const [name, value] of Object.entries(colors)) {
if (typeof value === 'string') {
html += `
<div class="color-swatch">
<div class="swatch" style="background: ${value}"></div>
<div class="info">
<span class="name">${prefix}${name}</span>
<span class="value">${value}</span>
</div>
</div>
`;
} else if (typeof value === 'object') {
html += generateColorSwatches(value, `${prefix}${name}-`);
}
}
html += '</div>';
return html;
}
function generateSpacingDemo(spacing) {
let html = '<div class="spacing-demos">';
for (const [name, value] of Object.entries(spacing)) {
html += `
<div class="spacing-item">
<div class="bar" style="width: ${value}"></div>
<span class="name">${name}</span>
<span class="value">${value}</span>
</div>
`;
}
html += '</div>';
return html;
}
const template = `
<!DOCTYPE html>
<html>
<head>
<title>Design Tokens 文档</title>
<style>
/* 文档样式 */
.color-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
.color-swatch { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.swatch { height: 80px; }
.info { padding: 12px; }
.name { display: block; font-weight: 600; }
.value { color: #6b7280; font-family: monospace; }
.spacing-item { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.bar { height: 24px; background: #3b82f6; }
</style>
</head>
<body>
<h1>Design Tokens</h1>
<h2>颜色</h2>
${generateColorSwatches(tokens.color)}
<h2>间距</h2>
${generateSpacingDemo(tokens.spacing)}
</body>
</html>
`;
fs.writeFileSync('./docs/tokens.html', template);
console.log('✅ Token 文档生成完成');
Figma 同步
Figma Tokens 插件集成
// tokens.json (Figma Tokens 格式)
{
"global": {
"colors": {
"blue": {
"500": {
"value": "#3b82f6",
"type": "color"
}
}
}
},
"light": {
"text": {
"primary": {
"value": "{colors.gray.900}",
"type": "color"
}
}
},
"dark": {
"text": {
"primary": {
"value": "{colors.gray.100}",
"type": "color"
}
}
}
}
GitHub Actions 自动同步
# .github/workflows/sync-tokens.yml
name: Sync Design Tokens
on:
push:
paths:
- 'tokens/**'
branches:
- main
jobs:
build-and-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build tokens
run: npm run tokens:build
- name: Commit built tokens
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add build/
git diff --staged --quiet || git commit -m "chore: update built tokens"
git push
- name: Generate documentation
run: npm run tokens:docs
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
Token 版本管理
语义化版本控制
// tokens/meta.json
{
"version": "2.1.0",
"lastUpdated": "2024-12-25",
"changelog": {
"2.1.0": {
"date": "2024-12-25",
"changes": [
"新增 feedback 颜色语义 tokens",
"调整 spacing.component.gap 从 12px 到 16px"
],
"breaking": []
},
"2.0.0": {
"date": "2024-12-01",
"changes": [
"重构 token 命名结构",
"新增暗色主题支持"
],
"breaking": [
"color.primary 重命名为 color.interactive.primary"
]
}
}
}
废弃标记
{
"color": {
"primary": {
"value": "{color.interactive.primary}",
"deprecated": true,
"deprecatedMessage": "请使用 color.interactive.primary 替代"
}
}
}
最佳实践总结
Design Tokens 最佳实践:
命名规范:
✓ 使用语义化命名(what),而非描述性命名(how)
✓ 采用一致的命名层级(category/type/item/state)
✓ 使用小写和连字符
✓ 避免使用具体数值作为名称
架构设计:
✓ 采用三层架构(基础/别名/语义)
✓ 语义层引用别名层,别名层引用基础层
✓ 主题只修改语义层的映射关系
工具链:
✓ 使用 Style Dictionary 或类似工具
✓ 自动生成多种输出格式
✓ 集成到 CI/CD 流程
✓ 自动生成文档
维护策略:
✓ 版本化管理 tokens
✓ 记录变更日志
✓ 提供废弃警告和迁移指南
✓ 定期审查和清理未使用的 tokens
团队协作:
✓ 设计师和开发者使用相同的 token
✓ Figma 变量与代码 tokens 同步
✓ 建立审核流程
✓ 培训团队成员使用 tokens
Design Tokens 是现代设计系统的基石,它们让设计决策变得可追踪、可管理、可扩展。投入时间建立完善的 Token 系统,将在长期维护中获得巨大回报。


