Разработка компонентной библиотеки для веб-приложения
Компонентная библиотека — это набор UI-компонентов с единым стилем, поведением и API, который используется несколькими командами или проектами. Разница с набором компонентов в одном репозитории: библиотека — отдельный пакет, который устанавливается как зависимость (npm install @company/ui), имеет версионирование и Changelog.
Принимать решение о создании библиотеки стоит осознанно: это серьёзная инфраструктурная инвестиция. Оправдана, если у вас несколько фронтенд-приложений, несколько команд или частая проблема «в каждом проекте Button выглядит по-разному».
Архитектура библиотеки
Monorepo vs отдельный репозиторий
Monorepo (Turborepo, Nx) удобен, если библиотека и приложения развиваются параллельно одной командой. Изменение компонента сразу видно во всех приложениях без публикации пакета.
Отдельный репозиторий с публикацией в npm (или приватный Registry: GitHub Packages, Verdaccio) — лучше при нескольких независимых командах. Потребляющая команда сама выбирает версию, когда обновляться.
Структура пакета:
packages/ui/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ └── ...
│ ├── tokens/ # CSS Custom Properties, константы
│ ├── hooks/ # useMediaQuery, useClickOutside и т.д.
│ └── index.ts # публичный API
├── package.json
└── tsconfig.json
Публичный API — критически важно контролировать, что экспортируется из index.ts. Всё, что экспортировано — это public API с обязательством поддерживать обратную совместимость.
Сборка и бандлинг
Для библиотеки компонентов сборщик — не Vite (для приложений), а специализированные инструменты:
tsup — самый простой. Одна команда, поддерживает ESM + CJS одновременно, TypeScript из коробки:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
Rollup — больше контроля, но сложнее конфигурация. Используется для сложных случаев (tree-shaking per-component, multiple entry points).
Vite Library Mode — если уже используете Vite. build.lib конфиг, форматы es + cjs.
Важно: CSS не бандлится внутрь JS-бандла. Если используете Tailwind — потребляющий проект должен сам запускать Tailwind с путями до компонентов библиотеки в content. Если CSS-in-JS (styled-components, Emotion) — стили едут с JS. Если CSS Modules — нужна отдельная сборка CSS.
Система токенов
Дизайн-токены — основа консистентности. В коде это CSS Custom Properties:
/* tokens.css */
:root {
--color-primary-500: #3B82F6;
--color-primary-600: #2563EB;
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--radius-sm: 4px;
--radius-md: 8px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-4: 16px;
}
Токены могут генерироваться из Figma Variables через плагин Tokens Studio или CLI Style Dictionary (Amazom). Style Dictionary принимает JSON с токенами и генерирует CSS, JS-константы, iOS Swift, Android XML одновременно.
API дизайн компонентов
Хороший API компонента — предсказуемый и минимальный. Принципы:
Controlled vs Uncontrolled. Input может быть controlled (управляется извне через value + onChange) и uncontrolled (управляет своим state сам через defaultValue). Библиотечные компоненты должны поддерживать оба режима.
Polymorphic компоненты. Button должен рендерить <button> по умолчанию, но с as="a" — <a>. В TypeScript это реализуется через generic:
type ButtonProps<T extends React.ElementType = 'button'> = {
as?: T;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
} & React.ComponentPropsWithoutRef<T>;
Composition через slot-паттерн. Вместо leftIcon и rightIcon props — <Button.Icon position="left"><SearchIcon /></Button.Icon>. Гибче, но сложнее API. Выбор зависит от сложности компонентов.
Radix UI Primitives — рекомендую как base для сложных компонентов (Select, Dialog, Tooltip, Dropdown). Они берут на себя accessibility, keyboard navigation, ARIA-атрибуты — ваша задача только стилизация. shadcn/ui — пример того, как обернуть Radix в Tailwind-стили.
Тестирование
Три уровня тестирования компонентов:
Unit тесты — Vitest + Testing Library. Проверяем логику, состояния, accessibility:
test('Button renders disabled state', () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
Visual regression — Chromatic (коммерческий, интегрируется со Storybook) или Playwright скриншоты. Каждый PR проверяет, не изменился ли визуальный вид компонентов.
Accessibility — axe-core через jest-axe или Storybook addon a11y. Автоматически находит очевидные ARIA-нарушения.
Версионирование и Breaking Changes
Семантическое версионирование (semver): MAJOR.MINOR.PATCH.
- PATCH: bugfix без изменения API
- MINOR: новый компонент или новый опциональный prop
- MAJOR: удаление компонента, переименование prop, изменение поведения
Инструменты для автоматизации: Changesets (Atlassian) — разработчик добавляет .changeset/*.md файл с описанием изменений, CI автоматически обновляет версию и публикует в npm.
Документация
Storybook — стандарт для UI-библиотек. Дополнительно:
- README.md каждого компонента с примерами использования
- Migration guides для MAJOR-версий
- Changelog — автоматически из Changesets
Сроки
| Этап | Время |
|---|---|
| Проектирование архитектуры, выбор toolchain | 3–5 дней |
| Настройка сборки, CI, versioning | 2–3 дня |
| Базовые компоненты (Button, Input, Checkbox, Select, Modal) | 10–15 дней |
| Сложные компоненты (DataTable, DatePicker, RichTextEditor) | 10–20 дней |
| Токены, темизация (light/dark) | 3–5 дней |
| Storybook + тесты | 5–8 дней |
| Документация и первый публичный релиз | 3–5 дней |
Минимальная MVP-библиотека с 15–20 компонентами, Storybook и CI: 6–10 недель. Полноценная библиотека уровня корпоративного продукта с 40+ компонентами, visual regression, A11y-тестами — от 4 до 6 месяцев итеративной разработки.







