Система 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
- Хаос в слоях токенов: Размытые границы между глобальными/семантическими/компонентными слоями, чрезмерно длинные цепочки ссылок
- Производительность переключения тем: Переключение тем во время выполнения вызывает мерцание перерисовки
- Мультибрендовое расширение: Большие различия в токенах между брендами, сложное наследование
- Сборочный вывод: Различные форматы вывода на разных платформах (CSS/JS/Swift/Kotlin)
- Дизайнерская коллаборация: Дизайнеры используют 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-переменной |
Продвинутая оптимизация
- Интеграция с Token Studio: Плагин Figma для двунаправленной синхронизации токенов дизайн-код
- Версионирование токенов: Семантическое версионирование + автоматическая генерация changelog
- Токены для A/B-тестов: Динамическое переключение токенов во время выполнения для визуального A/B-тестирования
- Token Lint: Плагин ESLint для обнаружения жёстко заданных дизайнерских значений в коде
- Сайт документации токенов: Автоматически генерируемая визуальная документация токенов
Сравнение
| Измерение | 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 будет стимулировать стандартизацию токенов, делая мультибрендовые и динамические токены времени выполнения стандартными возможностями корпоративных дизайн-систем.
Рекомендуемые онлайн-инструменты
- Форматирование JSON: /ru/json/format
- Калькулятор хешей: /ru/encode/hash
- cURL в код: /ru/dev/curl-to-code
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →