Design Tokens 设计与管理完全指南

HTMLPAGE 团队
17 分钟阅读

深入理解 Design Tokens 的概念与实践,学会建立和管理设计令牌系统,实现设计与开发的无缝协作,打造可扩展的设计系统基础。

#Design Tokens #设计系统 #CSS 变量 #主题切换 #设计工程

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 系统,将在长期维护中获得巨大回报。