У масштабних організаціях із багатьма цифровими продуктами (веб-додатки, мобільні клієнти 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-контрактів.