Система Design Tokens на практике: 5 паттернов для масштабируемой архитектуры тем и мультибрендовости

前端工程

Design Tokens: Атомарная инфраструктура дизайн-систем

Главная боль дизайн-систем — не нехватка компонентов, а переключение тем через переопределения, расширение брендов через дублирование и тёмный режим через заплатки. Design Tokens абстрагируют дизайнерские решения — цвета, отступы, типографику — в платформенно-независимые пары ключ-значение, позволяя «определить один раз, использовать везде». К 2026 году спецификация W3C Design Token достигла статуса Candidate Recommendation, Style Dictionary 4.0+ поддерживает мультиплатформенный вывод, и Design Tokens стали стандартной инфраструктурой дизайн-систем.

Эта статья описывает 5 ключевых паттернов, покрывая весь конвейер от слоёв токенов → переключения тем → мультибрендовости → сборочного конвейера → динамики времени выполнения.


Основные концепции

Концепция Описание
Design Token Атомарное представление дизайнерского решения в виде пары ключ-значение
Глобальный токен Сырые глобальные значения, напр., blue-500: #3b82f6
Токен-псевдоним Семантический псевдоним, напр., primary: {blue-500}
Токен компонента Токен на уровне компонента, напр., button-primary-bg: {primary}
Тема Набор значений токенов, напр., светлая/тёмная тема
Мультибрендовость Одна дизайн-система поддерживает несколько наборов токенов бренда
Style Dictionary Инструмент компиляции токенов с мультиплатформенным выводом
W3C DTCG Спецификация Design Token Community Group

Анализ проблем: 5 вызовов в системах Design Tokens

  1. Хаос в слоях токенов: Размытые границы между глобальными/семантическими/компонентными слоями, чрезмерно длинные цепочки ссылок
  2. Производительность переключения тем: Переключение тем во время выполнения вызывает мерцание перерисовки
  3. Мультибрендовое расширение: Большие различия в токенах между брендами, сложное наследование
  4. Сборочный вывод: Различные форматы вывода на разных платформах (CSS/JS/Swift/Kotlin)
  5. Дизайнерская коллаборация: Дизайнеры используют Figma, разработчики — код, синхронизация токенов затруднена

Пошагово: 5 паттернов Design Tokens

Паттерн 1: Трёхслойная архитектура токенов

// tokens/global/color.json
{
  "color": {
    "blue": {
      "100": { "value": "#dbeafe", "type": "color" },
      "500": { "value": "#3b82f6", "type": "color" },
      "900": { "value": "#1e3a8a", "type": "color" }
    },
    "gray": {
      "50": { "value": "#f9fafb", "type": "color" },
      "900": { "value": "#111827", "type": "color" }
    },
    "red": {
      "500": { "value": "#ef4444", "type": "color" }
    },
    "green": {
      "500": { "value": "#22c55e", "type": "color" }
    }
  }
}
// tokens/semantic/color.json
{
  "color": {
    "primary": { "value": "{color.blue.500}", "type": "color" },
    "primary-hover": { "value": "{color.blue.600}", "type": "color" },
    "background": { "value": "{color.gray.50}", "type": "color" },
    "surface": { "value": "#ffffff", "type": "color" },
    "text": { "value": "{color.gray.900}", "type": "color" },
    "text-secondary": { "value": "{color.gray.500}", "type": "color" },
    "error": { "value": "{color.red.500}", "type": "color" },
    "success": { "value": "{color.green.500}", "type": "color" },
    "border": { "value": "{color.gray.200}", "type": "color" }
  }
}
// tokens/component/button.json
{
  "button": {
    "primary": {
      "bg": { "value": "{color.primary}", "type": "color" },
      "bg-hover": { "value": "{color.primary-hover}", "type": "color" },
      "text": { "value": "#ffffff", "type": "color" },
      "border-radius": { "value": "{radius.md}", "type": "borderRadius" },
      "padding-x": { "value": "{spacing.4}", "type": "spacing" },
      "padding-y": { "value": "{spacing.2}", "type": "spacing" }
    },
    "secondary": {
      "bg": { "value": "{color.surface}", "type": "color" },
      "text": { "value": "{color.primary}", "type": "color" },
      "border": { "value": "{color.border}", "type": "color" }
    }
  }
}

Паттерн 2: Переключение тем (Светлая/Тёмная)

// tokens/semantic/color.dark.json
{
  "color": {
    "primary": { "value": "{color.blue.400}", "type": "color" },
    "background": { "value": "{color.gray.900}", "type": "color" },
    "surface": { "value": "#1f2937", "type": "color" },
    "text": { "value": "{color.gray.50}", "type": "color" },
    "text-secondary": { "value": "{color.gray.400}", "type": "color" },
    "border": { "value": "{color.gray.700}", "type": "color" }
  }
}
/* Вывод сборки: tokens.css */
:root {
  --color-primary: #3b82f6;
  --color-background: #f9fafb;
  --color-surface: #ffffff;
  --color-text: #111827;
  --color-border: #e5e7eb;
  --button-primary-bg: var(--color-primary);
  --button-primary-text: #ffffff;
}

[data-theme="dark"] {
  --color-primary: #60a5fa;
  --color-background: #111827;
  --color-surface: #1f2937;
  --color-text: #f9fafb;
  --color-border: #374151;
  --button-primary-bg: var(--color-primary);
}
// Переключатель тем
type Theme = 'light' | 'dark' | 'system';

class ThemeManager {
  private mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  private listeners: Set<(theme: Theme) => void> = new Set();

  init() {
    const saved = localStorage.getItem('theme') as Theme | null;
    if (saved && saved !== 'system') {
      this.apply(saved);
    } else {
      this.apply(this.mediaQuery.matches ? 'dark' : 'light');
    }
    this.mediaQuery.addEventListener('change', (e) => {
      if (this.getStored() === 'system') {
        this.apply(e.matches ? 'dark' : 'light');
      }
    });
  }

  setTheme(theme: Theme) {
    localStorage.setItem('theme', theme);
    if (theme === 'system') {
      this.apply(this.mediaQuery.matches ? 'dark' : 'light');
    } else {
      this.apply(theme);
    }
    this.listeners.forEach((fn) => fn(theme));
  }

  private apply(mode: 'light' | 'dark') {
    document.documentElement.setAttribute('data-theme', mode);
  }

  private getStored(): Theme {
    return (localStorage.getItem('theme') as Theme) || 'system';
  }

  onChange(fn: (theme: Theme) => void) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }
}

Паттерн 3: Наследование токенов мультибрендовости

// brands/brand-a/tokens.json
{
  "color": {
    "primary": { "value": "#3b82f6", "type": "color", "$extensions": { "brand": "A" } },
    "primary-hover": { "value": "#2563eb", "type": "color" }
  },
  "typography": {
    "font-family": { "value": "Inter, sans-serif", "type": "fontFamily" },
    "heading-weight": { "value": "700", "type": "fontWeight" }
  },
  "radius": {
    "base": { "value": "8px", "type": "borderRadius" }
  }
}
// brands/brand-b/tokens.json
{
  "color": {
    "primary": { "value": "#8b5cf6", "type": "color", "$extensions": { "brand": "B" } },
    "primary-hover": { "value": "#7c3aed", "type": "color" }
  },
  "typography": {
    "font-family": { "value": "Poppins, sans-serif", "type": "fontFamily" },
    "heading-weight": { "value": "600", "type": "fontWeight" }
  },
  "radius": {
    "base": { "value": "4px", "type": "borderRadius" }
  }
}
// Переключатель брендов
type Brand = 'brand-a' | 'brand-b';

async function switchBrand(brand: Brand) {
  const brandCSS = await fetch(`/tokens/${brand}.css`).then((r) => r.text());
  let style = document.getElementById('brand-tokens') as HTMLStyleElement;
  if (!style) {
    style = document.createElement('style');
    style.id = 'brand-tokens';
    document.head.appendChild(style);
  }
  style.textContent = brandCSS;
  document.documentElement.setAttribute('data-brand', brand);
  localStorage.setItem('brand', brand);
}

Паттерн 4: Сборочный конвейер Style Dictionary

// style-dictionary.config.mjs
import StyleDictionary from 'style-dictionary';

StyleDictionary.registerTransform({
  name: 'size/pxToRem',
  type: 'value',
  matcher: (token) => token.type === 'spacing' || token.type === 'fontSize',
  transformer: (token) => {
    const base = 16;
    const val = parseFloat(token.value);
    return `${val / base}rem`;
  },
});

StyleDictionary.registerTransformGroup({
  name: 'custom/css',
  transforms: [
    'attribute/cti',
    'name/kebab',
    'time/seconds',
    'size/pxToRem',
    'color/css',
  ],
});

const config = {
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'custom/css',
      buildPath: 'dist/css/',
      files: [
        {
          destination: 'tokens.css',
          format: 'css/variables',
          options: { outputReferences: true },
        },
      ],
    },
    js: {
      transformGroup: 'js',
      buildPath: 'dist/js/',
      files: [
        {
          destination: 'tokens.js',
          format: 'javascript/es6',
        },
        {
          destination: 'tokens.d.ts',
          format: 'typescript/es6-declarations',
        },
      ],
    },
    json: {
      transformGroup: 'json',
      buildPath: 'dist/json/',
      files: [
        {
          destination: 'tokens.json',
          format: 'json',
        },
      ],
    },
    ios: {
      transformGroup: 'ios-swift-separate',
      buildPath: 'dist/ios/',
      files: [
        {
          destination: 'DesignTokens.swift',
          format: 'ios-swift/class.swift',
          className: 'DesignTokens',
        },
      ],
    },
    android: {
      transformGroup: 'android',
      buildPath: 'dist/android/',
      files: [
        {
          destination: 'design_tokens.xml',
          format: 'android/resources',
        },
      ],
    },
  },
};

export default config;
# Сборка всех платформ
npx style-dictionary build

# Отслеживание изменений токенов
npx style-dictionary build --watch

Паттерн 5: Динамические токены времени выполнения

// runtime-token-engine.ts
interface TokenValue {
  value: string;
  type: string;
  description?: string;
}

class RuntimeTokenEngine {
  private tokens: Map<string, TokenValue> = new Map();
  private subscribers: Map<string, Set<(value: string) => void>> = new Map();

  loadFromCSS() {
    const styles = getComputedStyle(document.documentElement);
    const allVars = Array.from(document.styleSheets)
      .flatMap((sheet) => {
        try {
          return Array.from(sheet.cssRules);
        } catch {
          return [];
        }
      })
      .filter((rule) => rule instanceof CSSStyleRule)
      .flatMap((rule: CSSStyleRule) => {
        const props: string[] = [];
        for (let i = 0; i < rule.style.length; i++) {
          const prop = rule.style[i];
          if (prop.startsWith('--')) {
            props.push(prop);
          }
        }
        return props;
      });

    const uniqueVars = [...new Set(allVars)];
    uniqueVars.forEach((v) => {
      const val = styles.getPropertyValue(v).trim();
      if (val) {
        this.tokens.set(v, { value: val, type: 'unknown' });
      }
    });
  }

  setToken(name: string, value: string) {
    const varName = name.startsWith('--') ? name : `--${name}`;
    document.documentElement.style.setProperty(varName, value);
    this.tokens.set(varName, { value, type: 'unknown' });
    this.notify(varName, value);
  }

  getToken(name: string): string | undefined {
    const varName = name.startsWith('--') ? name : `--${name}`;
    return getComputedStyle(document.documentElement)
      .getPropertyValue(varName)
      .trim();
  }

  subscribe(name: string, callback: (value: string) => void) {
    const varName = name.startsWith('--') ? name : `--${name}`;
    if (!this.subscribers.has(varName)) {
      this.subscribers.set(varName, new Set());
    }
    this.subscribers.get(varName)!.add(callback);
    return () => this.subscribers.get(varName)?.delete(callback);
  }

  private notify(name: string, value: string) {
    this.subscribers.get(name)?.forEach((fn) => fn(value));
  }
}

Руководство по ловушкам

Ловушка 1: Слишком глубокие цепочки ссылок токенов

// ❌ Неправильно: 5-уровневая цепочка ссылок, сложно отлаживать
{
  "btn-bg": { "value": "{button.primary.bg}" },
  "button.primary.bg": { "value": "{color.primary}" },
  "color.primary": { "value": "{blue.500}" }
}

// ✅ Правильно: Держать в пределах 3 уровней
{
  "button-primary-bg": { "value": "{color.primary}" },
  "color.primary": { "value": "#3b82f6" }
}

Ловушка 2: Жёстко заданные значения цветов

/* ❌ Неправильно: Жёстко заданные цвета */
.card { background: #ffffff; border: 1px solid #e5e7eb; }

/* ✅ Правильно: Использовать переменные токенов */
.card { background: var(--color-surface); border: 1px solid var(--color-border); }

Ловушка 3: Мерцание при переключении темы

<!-- ❌ Неправильно: Тема устанавливается после загрузки JS, вызывая мерцание -->

<!-- ✅ Правильно: Встроенный скрипт блокирует рендеринг -->
<script>
  (function() {
    const theme = localStorage.getItem('theme');
    if (theme === 'dark' || (!theme && matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  })();
</script>

Ловушка 4: Чрезмерное использование токенов компонентов

// ❌ Неправильно: Создание токена для каждого свойства компонента
{
  "card-padding-top": { "value": "{spacing.4}" },
  "card-padding-right": { "value": "{spacing.4}" },
  "card-padding-bottom": { "value": "{spacing.4}" },
  "card-padding-left": { "value": "{spacing.4}" }
}

// ✅ Правильно: Переиспользовать семантические токены
{
  "card-padding": { "value": "{spacing.4}" }
}

Ловушка 5: Игнорирование валидации типов токенов

// ❌ Неправильно: Без валидации типов, токен цвета получает значение отступа
// ✅ Правильно: Валидировать во время сборки
StyleDictionary.registerFilter({
  name: 'isValidColor',
  matcher: (token) => {
    if (token.type === 'color') {
      return /^#([0-9a-f]{3}){1,2}$/i.test(token.value) ||
             token.value.startsWith('{');
    }
    return true;
  },
});

Устранение ошибок

# Сообщение об ошибке Причина Решение
1 Reference not found: {color.xxx} Токен ссылается на несуществующий ключ Проверить написание пути ссылки
2 Circular reference detected Круговая ссылка токенов Разорвать цепочку круговой ссылки
3 Invalid token value Неверный формат значения токена Валидировать формат цвета/отступа
4 CSS variable not applied Специфичность селектора переопределена Увеличить специфичность селектора CSS-переменной
5 Theme flash on load Инициализация темы после рендеринга Встроенный блокирующий скрипт для установки темы
6 Style Dictionary build failed Ошибка формата файла токенов Проверить синтаксис JSON и $extensions
7 Token not found in output Токен не сопоставлён источником Проверить glob-путь source
8 Platform output mismatch Несогласованные правила трансформации Унифицировать конфигурацию transformGroup
9 Figma sync conflict Токены Figma не синхронизированы с кодом Использовать Token Studio для двунаправленной синхронизации
10 Runtime token override fails Переопределение времени выполнения не работает Проверить приоритет и область CSS-переменной

Продвинутая оптимизация

  1. Интеграция с Token Studio: Плагин Figma для двунаправленной синхронизации токенов дизайн-код
  2. Версионирование токенов: Семантическое версионирование + автоматическая генерация changelog
  3. Токены для A/B-тестов: Динамическое переключение токенов во время выполнения для визуального A/B-тестирования
  4. Token Lint: Плагин ESLint для обнаружения жёстко заданных дизайнерских значений в коде
  5. Сайт документации токенов: Автоматически генерируемая визуальная документация токенов

Сравнение

Измерение CSS-переменные Design Tokens Конфигурация Tailwind Тема CSS-in-JS
Поддержка платформ Только Web Web/iOS/Android Только Web Только Web
Типобезопасность
Переключение тем
Мультибрендовость Вручную Нативная поддержка Переключение конфигурации Переключение конфигурации
Дизайнерская коллаборация ✅ Синхронизация Figma
Сборочный вывод CSS CSS/JS/Swift/Kotlin CSS/JS JS

Итоги: Системы Design Tokens — ключевая инфраструктура для эволюции дизайн-систем от «библиотек компонентов» к «языкам дизайна». Трёхслойная архитектура токенов (глобальные → семантические → компонентные) в сочетании с мультиплатформенным выводом Style Dictionary устанавливает единый источник истины для дизайнерских решений. В 2026 году созревающая спецификация W3C DTCG будет стимулировать стандартизацию токенов, делая мультибрендовые и динамические токены времени выполнения стандартными возможностями корпоративных дизайн-систем.


Рекомендуемые онлайн-инструменты

Попробуйте эти локальные браузерные инструменты — регистрация не требуется →

#Design Token#设计系统#主题切换#CSS变量#2026#前端工程