Антон Ларичев

Введение
Дизайн-система в React — это набор переиспользуемых компонентов, дизайн-токенов и правил, которые обеспечивают единообразие интерфейса во всём приложении. Без неё каждый разработчик в команде изобретает свои отступы, цвета и размеры кнопок, а продукт превращается в лоскутное одеяло из несогласованных стилей.
В этой статье разберём, как построить дизайн-систему в React с нуля: от определения дизайн-токенов до создания компонентов и их документирования в Storybook. Рассмотрим практический пример с TypeScript и CSS-переменными.
Что такое дизайн-токены и зачем они нужны
Дизайн-токены — это базовые значения визуального языка: цвета, шрифты, отступы, радиусы скруглений, тени. Вместо того чтобы разбрасывать магические числа по коду, токены хранят все значения в одном месте.
Трёхуровневая структура токенов помогает масштабировать систему:
// tokens/colors.ts — базовые (core) токены
export const coreColors = {
purple100: '#f3e8ff',
purple500: '#8b5cf6',
purple900: '#4c1d95',
gray100: '#f3f4f6',
gray900: '#111827',
white: '#ffffff',
} as const;
// tokens/semantic.ts — семантические токены
export const semanticColors = {
textPrimary: coreColors.gray900,
textSecondary: coreColors.purple900,
backgroundPrimary: coreColors.white,
backgroundAccent: coreColors.purple100,
borderDefault: coreColors.gray100,
brandPrimary: coreColors.purple500,
} as const;
Семантические токены не привязаны к конкретному цвету — они описывают назначение. Это позволяет легко добавить тёмную тему, поменяв только маппинг семантических токенов на базовые.
Как превратить токены в CSS-переменные
Чтобы дизайн-токены работали в стилях, преобразуем их в CSS-переменные:
// tokens/cssVariables.ts
import { semanticColors } from './semantic';
export function applyTokens(element: HTMLElement = document.documentElement) {
const tokenMap: Record<string, string> = {
'--color-text-primary': semanticColors.textPrimary,
'--color-text-secondary': semanticColors.textSecondary,
'--color-bg-primary': semanticColors.backgroundPrimary,
'--color-bg-accent': semanticColors.backgroundAccent,
'--color-border': semanticColors.borderDefault,
'--color-brand': semanticColors.brandPrimary,
};
Object.entries(tokenMap).forEach(([key, value]) => {
element.style.setProperty(key, value);
});
}
Теперь в CSS компонентов используем переменные вместо жёстких значений:
.button-primary {
background-color: var(--color-brand);
color: var(--color-bg-primary);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
}
Как создать компоненты дизайн-системы в React
Компоненты дизайн-системы строятся по принципу Atomic Design: от простых атомов (кнопка, инпут) к молекулам (поле ввода с лейблом) и организмам (форма). Начнём с базового компонента Button.
// components/Button/Button.tsx
import React from 'react';
import styles from './Button.module.css';
type ButtonVariant = 'primary' | 'secondary' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
isLoading = false,
children,
disabled,
className,
...props
}) => {
return (
<button
className={`${styles.button} ${styles[variant]} ${styles[size]} ${className ?? ''}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? <span className={styles.spinner} /> : children}
</button>
);
};
/* components/Button/Button.module.css */
.button {
font-family: var(--font-family);
font-weight: 500;
border: none;
cursor: pointer;
transition: background-color 0.2s, opacity 0.2s;
}
.primary {
background-color: var(--color-brand);
color: var(--color-bg-primary);
}
.secondary {
background-color: var(--color-bg-accent);
color: var(--color-text-secondary);
}
.ghost {
background-color: transparent;
color: var(--color-text-primary);
}
.sm { padding: var(--spacing-xs) var(--spacing-sm); font-size: 14px; }
.md { padding: var(--spacing-sm) var(--spacing-md); font-size: 16px; }
.lg { padding: var(--spacing-md) var(--spacing-lg); font-size: 18px; }
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Каждый компонент дизайн-системы использует токены через CSS-переменные. Если дизайнер изменит основной цвет бренда — достаточно обновить один токен, и все компоненты подхватят изменение.
Как настроить Storybook для React компонентов
Storybook позволяет разрабатывать и документировать компоненты UI kit изолированно от основного приложения. Установка:
npx storybook@latest init
После инициализации создаём stories для компонентов:
// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// Основной вариант кнопки
export const Primary: Story = {
args: {
children: 'Отправить',
variant: 'primary',
size: 'md',
},
};
// Вторичный вариант
export const Secondary: Story = {
args: {
children: 'Отмена',
variant: 'secondary',
},
};
// Состояние загрузки
export const Loading: Story = {
args: {
children: 'Сохранение...',
isLoading: true,
},
};
Для документирования токенов используйте аддон storybook-design-token, который автоматически генерирует таблицы цветов, шрифтов и отступов из CSS-переменных:
npm install storybook-design-token
Частые ошибки при создании дизайн-системы
Жёсткие значения вместо токенов. Если в компоненте написано color: #8b5cf6 вместо var(--color-brand), при смене темы этот компонент останется прежним. Все визуальные значения должны идти через токены.
Слишком много вариантов у компонента. Кнопка с 15 пропсами сложнее в поддержке, чем три отдельных компонента. Начинайте с минимального API и расширяйте по мере реальных потребностей.
Отсутствие документации. Компонент без Story в Storybook — это компонент, который никто не будет переиспользовать. Документируйте каждый компонент сразу при создании.
Пропуск семантического уровня токенов. Если компоненты ссылаются напрямую на purple500, добавление тёмной темы потребует правок в каждом файле. Семантический слой (brandPrimary) решает эту проблему.
Заключение
Дизайн-система в React — это инвестиция, которая окупается с ростом проекта. Начните с определения дизайн-токенов на трёх уровнях (базовые, семантические, компонентные), создайте переиспользуемые компоненты с минимальным API и задокументируйте их в Storybook. Такой подход обеспечит консистентность интерфейса, ускорит разработку и упростит поддержку тем оформления.



Комментарии
0