Skip to content

Design Token 系統:從 CSS 變數到多主題架構

作為元件系統負責人,設計 Token 是我們團隊今年的重點建設之一。分享一下從 0 到 1 搭建 Token 系統的經驗。

什麼是 Design Token

Design Token 是設計決策的最小單元。顏色、間距、圓角、字型——這些值不應該散落在 CSS 檔案裡,而應該集中定義、統一管理。

typescript
// tokens/colors.ts
export const colors = {
  // 基礎色板
  gray: {
    50: "#f9fafb",
    100: "#f3f4f6",
    200: "#e5e7eb",
    300: "#d1d5db",
    400: "#9ca3af",
    500: "#6b7280",
    600: "#4b5563",
    700: "#374151",
    800: "#1f2937",
    900: "#111827",
  },
  primary: {
    50: "#eff6ff",
    500: "#3b82f6",
    600: "#2563eb",
    700: "#1d4ed8",
  },
} as const;

這還只是原始值。真正有用的是語義化 Token:

typescript
// tokens/semantic.ts
export const semanticColors = {
  light: {
    bg: {
      primary: colors.white,
      secondary: colors.gray[50],
      elevated: colors.white,
    },
    text: {
      primary: colors.gray[900],
      secondary: colors.gray[600],
      disabled: colors.gray[400],
    },
    border: {
      default: colors.gray[200],
      focus: colors.primary[500],
    },
  },
  dark: {
    bg: {
      primary: colors.gray[900],
      secondary: colors.gray[800],
      elevated: colors.gray[800],
    },
    text: {
      primary: colors.gray[50],
      secondary: colors.gray[400],
      disabled: colors.gray[600],
    },
    border: {
      default: colors.gray[700],
      focus: colors.primary[500],
    },
  },
};

CSS 變數方案

typescript
// 生成 CSS 變數
function generateCSSVars(theme: "light" | "dark") {
  const tokens = semanticColors[theme];
  const vars: Record<string, string> = {};

  function flatten(obj: Record<string, any>, prefix: string) {
    for (const [key, value] of Object.entries(obj)) {
      if (typeof value === "object") {
        flatten(value, `${prefix}-${key}`);
      } else {
        vars[`${prefix}-${key}`] = value;
      }
    }
  }

  flatten(tokens, "--color");
  return vars;
}
css
/* 自動生成 */
:root {
  --color-bg-primary: #ffffff;
  --color-bg-secondary: #f9fafb;
  --color-text-primary: #111827;
  --color-text-secondary: #4b5563;
  --color-border-default: #e5e7eb;
}

[data-theme="dark"] {
  --color-bg-primary: #111827;
  --color-bg-secondary: #1f2937;
  --color-text-primary: #f9fafb;
  --color-text-secondary: #9ca3af;
  --color-border-default: #374151;
}

元件中使用

tsx
// Button 元件
const Button = styled.button`
  background: var(--color-bg-elevated);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md);
  padding: var(--spacing-2) var(--spacing-4);
  font-size: var(--text-sm);

  &:hover {
    background: var(--color-bg-secondary);
  }

  &:focus-visible {
    outline: 2px solid var(--color-border-focus);
    outline-offset: 2px;
  }
`;

切換主題只需要改 data-theme 屬性,所有元件自動變色。不需要 JavaScript 重新渲染。

間距和排版 Token

typescript
export const spacing = {
  0: "0",
  1: "0.25rem",  // 4px
  2: "0.5rem",   // 8px
  3: "0.75rem",  // 12px
  4: "1rem",     // 16px
  6: "1.5rem",   // 24px
  8: "2rem",     // 32px
  12: "3rem",    // 48px
  16: "4rem",    // 64px
} as const;

export const typography = {
  xs: { fontSize: "0.75rem", lineHeight: "1rem" },
  sm: { fontSize: "0.875rem", lineHeight: "1.25rem" },
  base: { fontSize: "1rem", lineHeight: "1.5rem" },
  lg: { fontSize: "1.125rem", lineHeight: "1.75rem" },
  xl: { fontSize: "1.25rem", lineHeight: "1.75rem" },
} as const;

Token 工具鏈

jsonc
// package.json
{
  "scripts": {
    "tokens:build": "style-dictionary build",
    "tokens:watch": "style-dictionary build --watch"
  }
}

用 Style Dictionary 從 Token 原始檔生成 CSS 變數、TypeScript 型別、iOS/Android 常量。一套原始檔,多平臺輸出。

小結

  • Design Token 是設計系統的基礎設施,讓設計決策可程式設計
  • 基礎色板 + 語義化 Token 的兩層結構兼顧靈活性和可維護性
  • CSS 變數方案實現零 JS 開銷的主題切換
  • 配合 Style Dictionary 實現 Token 的多平臺分發
  • Token 系統的建設投入大,但在多主題、多品牌專案中收益明顯

MIT Licensed