Розробка компонентної бібліотеки для веб-додатка
Компонентна бібліотека — це набір 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, кілька entry points).
Vite Library Mode — якщо вже використовуєте Vite. build.lib конфіг, формати es + cjs.
Важливо: CSS не бандлиться всередину JS-бандла. Якщо використовуєте Tailwind — проект, що використовує, повинен сам запускати Tailwind зі шляхами до компонентів бібліотеки в content. Якщо CSS-in-JS (styled-components, Emotion) — стилі їдять з JS. Якщо CSS Modules — потрібна окремa збірка 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 (Amazon). 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>;
Композиція через slot-паттерн. Замість leftIcon та rightIcon props — <Button.Icon position="left"><SearchIcon /></Button.Icon>. Гнучче, але складніший API. Вибір залежить від складності компонентів.
Radix UI Primitives — рекомендую як базу для складних компонентів (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 місяців ітеративної розробки.







