В масштабных организациях с множеством цифровых продуктов (веб-приложения, мобильные клиенты iOS/Android, встраиваемые виджеты и админ-панели) рассинхронизация между дизайнерами в Figma и фронтенд-инженерами приводит к накоплению критического визуального долга:
#3b82f6 против #2563eb), случайные отступы (от 13px до 19px) и хаотичные радиусы скругления.Решением проблемы на архитектурном уровне выступает концепция Дизайн-токенов (Design Tokens).
Архитектура корпоративных дизайн-токенов строится на строгой трехуровневой иерархии:
color.blue.500 = #3b82f6, space.4 = 16px).surface.canvas = var(--color-slate-950), color.interactive = var(--color-blue-500)).button.primary.bg = var(--color-interactive)).В Tailwind CSS v4 архитектура стилизации перешла на нативный CSS-движок Lightning CSS на Rust, полностью упразднив конфигурационный файл tailwind.config.js в пользу директивы @theme и каскадных переменных CSS Custom Properties.
Компоненты никогда не должны напрямую использовать примитивные токены (например, bg-blue-500). Они потребляют исключительно семантические алиасы (bg-brand-primary). В Tailwind v4 директива @theme напрямую связывает скомпилированные переменные Style Dictionary с утилитарными классами без оверхеда на JavaScript-конфигурации.
Конвейер синхронизации токенов связывает единый источник правды в Figma с React-компонентами через автоматизированный пайплайн:
┌─────────────────────────────────────────────────────────────┐
│ FIGMA DESIGN SYSTEM (Variables) │
│ Modes: Light / Dark / High Contrast / Tenant Brand │
└──────────────────────────────┬──────────────────────────────┘
│ Экспорт через W3C Tokens Studio Plugin
▼
┌─────────────────────────────────────────────────────────────┐
│ DESIGN TOKENS REPOSITORY (DTCG JSON) │
│ tokens/primitives.json | tokens/semantic-dark.json │
└──────────────────────────────┬──────────────────────────────┘
│ Компиляция через Style Dictionary v4
▼
┌─────────────────────────────────────────────────────────────┐
│ GENERATED CSS VARIABLES & TYPES │
│ dist/variables.css | dist/tokens.d.ts │
└──────────────────────────────┬──────────────────────────────┘
│ Инъекция в CSS-слой
▼
┌─────────────────────────────────────────────────────────────┐
│ TAILWIND CSS v4 (@theme directive) │
│ @theme { │
│ --color-canvas: var(--color-semantic-surface-canvas); │
│ --color-brand-primary: var(--color-semantic-brand); │
│ } │
└──────────────────────────────┬──────────────────────────────┘
│ Строгая типизация через CVA
▼
┌─────────────────────────────────────────────────────────────┐
│ REACT 19 TYPED UI COMPONENTS (Button, Card) │
│ <Button variant="primary" size="lg">Submit</Button> │
└─────────────────────────────────────────────────────────────┘Определение глобальных цветов, шага сетки (4px/8px grid) и радиусов в JSON-формате W3C DTCG.
Создание семантических токенов surface, text, border, brand с привязкой к режимам темы (Light, Dark, High-Contrast).
Генерация CSS Custom Properties (variables.css) и TypeScript-деклараций типов для монорепозитория.
Подключение сгенерированных переменных через директиву @theme в главном globals.css файле.
Создание типобезопасных полиморфных компонентов (Button, Card, Input) с поддержкой пропа asChild.
Почему в кодовой базе UI-кита enterprise-уровня категорически запрещено обращаться к примитивным токенам напрямую (например, className='bg-violet-500')?
Исходный JSON-файл токенов оформляется по спецификации Design Tokens Community Group:
{
"color": {
"primitive": {
"violet": {
"500": { "$value": "#8b5cf6", "$type": "color" },
"600": { "$value": "#7c3aed", "$type": "color" },
"900": { "$value": "#4c1d95", "$type": "color" }
},
"slate": {
"900": { "$value": "#0f172a", "$type": "color" },
"950": { "$value": "#020617", "$type": "color" }
}
},
"semantic": {
"surface": {
"canvas": {
"$value": "{color.primitive.slate.950}",
"$type": "color"
}
},
"brand": {
"primary": {
"$value": "{color.primitive.violet.500}",
"$type": "color"
},
"hover": {
"$value": "{color.primitive.violet.600}",
"$type": "color"
}
}
}
}
}Конфигурационный файл транслирует JSON-токены в CSS Custom Properties с сохранением ссылочной целостности:
import StyleDictionary from 'style-dictionary';
const sd = new StyleDictionary({
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'src/tokens/dist/',
files: [
{
destination: 'variables.css',
format: 'css/variables',
options: {
outputReferences: true, // Сохранять var() ссылки между токенами
},
},
],
},
ts: {
transformGroup: 'js',
buildPath: 'src/tokens/dist/',
files: [
{
destination: 'tokens.ts',
format: 'javascript/es6',
},
{
destination: 'tokens.d.ts',
format: 'typescript/es6-declarations',
},
],
},
},
});
await sd.buildAllPlatforms();Сгенерированные CSS-переменные подключаются к директиве @theme в главном файле стилей приложения:
@import "tailwindcss";
@import "../tokens/dist/variables.css";
/* Регистрация дизайн-токенов в системе утилит Tailwind v4 */
@theme {
--color-canvas: var(--color-semantic-surface-canvas);
--color-brand-primary: var(--color-semantic-brand-primary);
--color-brand-hover: var(--color-semantic-brand-hover);
--font-sans: "Inter Variable", -apple-system, BlinkMacSystemFont, sans-serif;
--radius-badge: 0.375rem;
}
/* Переопределение переменных для темной темы */
:root[data-theme="dark"] {
--color-semantic-surface-canvas: #09090b;
--color-semantic-brand-primary: #a855f7;
--color-semantic-brand-hover: #9333ea;
}
/* Переопределение для режима высокой контрастности (WCAG AAA) */
:root[data-theme="high-contrast"] {
--color-semantic-surface-canvas: #000000;
--color-semantic-brand-primary: #ffff00;
--color-semantic-brand-hover: #ffffff;
}Дизайнер обновляет цвет бренда в Figma. Плагин генерирует стандартизированный tokens.json.
Валидация ссылок между токенами и сборка файлов variables.css и tokens.d.ts.
Движок на Rust генерирует утилитные классы bg-brand-primary, text-canvas на базе CSS-переменных.
Компонент Button скомпонован со строгими вариантами пропсов (variant, size) через CVA.
Каким образом предотвратить мерцание темы (Flash of Unstyled Theme - FOUT) при серверном рендеринге в Next.js App Router?
Ниже представлен компонент кнопки корпоративного уровня, использующий семантические токены Tailwind v4, утилиту class-variance-authority (CVA) и слот-паттерн (Slot / asChild) из Radix UI для типобезопасного полиморфизма:
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from '@radix-ui/react-slot';
export const buttonVariants = cva(
// Базовые стили: отступы, интерактивность, фокус, доступность
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-brand-primary text-white hover:bg-brand-hover shadow-sm',
secondary: 'bg-bg-elevated border border-border text-text-primary hover:bg-bg-elevated/80',
outline: 'border border-brand-primary/50 text-brand-primary hover:bg-brand-primary/10',
ghost: 'text-text-muted hover:text-text-primary hover:bg-bg-elevated/50',
danger: 'bg-danger text-white hover:bg-danger/90',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4 py-2',
lg: 'h-12 px-6 text-base',
icon: 'h-10 w-10 p-0',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/**
* При asChild=true компонент делегирует стили и обработчики первому JSX-ребенку (например, Next.js Link)
*/
asChild?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Component = asChild ? Slot : 'button';
return (
<Component
className={buttonVariants({ variant, size, className })}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';Использование произвольных значений className="bg-[#8b5cf6]" вместо семантических токенов className="bg-brand-primary" разрушает дизайн-систему: такой компонент останется слепяще-фиолетовым при переключении в темную тему или тему высокой контрастности.
// ❌ Антипаттерн: при смене темы кнопка не адаптируется
<button className="bg-[#8b5cf6] text-white px-4 py-2">Сохранить</button>
// ✅ Правильное решение: использование семантического класса Tailwind v4
<button className="bg-brand-primary text-white px-4 py-2">Сохранить</button>Использование примитивных токенов напрямую в компонентах (например, bg-violet-500 вместо bg-brand-primary). Если компания решит сменить основной цвет бренда с фиолетового на изумрудный, придется переписывать тысячи файлов вместо изменения одной строки в файле семантических токенов.
Если тема пользователя считывается из localStorage внутри клиентского хука useEffect, сервер всегда отрендерит светлую тему, а клиент через 100 мс переключит на темную, вызвав раздражающую белую вспышку.
Решение: сохранять тему в cookie и выставлять атрибут <html data-theme="..."> на сервере в SSR, либо инжектировать крошечный блокирующий скрипт в <head>.
Полиморфизм вида <Button as="a" href="/home"> ломает выведение типов в TypeScript (TypeScript не знает, какие пропсы валидны — для button или a). Паттерн asChild на базе @radix-ui/react-slot сохраняет 100% типобезопасность целевого элемента.
tailwind.config.js на нативную директиву @theme, компилируемую движком Lightning CSS на Rust.class-variance-authority) гарантирует строгую сквозную TypeScript-типизацию вариантов компонентов.asChild на базе @radix-ui/react-slot обеспечивает типобезопасный полиморфизм без потери HTML-контрактов.