Уявіть собі, що кожного разу, коли ви хочете створити веб-додаток, вам потрібно писати кнопку з нуля. Не просто <button>, а кнопку, яка:
Якщо ви напишете це один раз, вам знадобиться близько 150-200 рядків CSS та JavaScript. А тепер помножте це на 50+ компонентів, які потрібні для створення сучасного додатку (inputs, modals, dropdowns, tables, tooltips тощо).
Саме тому існують UI бібліотеки — щоб ви могли сфокусуватися на бізнес-логіці, а не переписувати checkbox у сотий раз.
Але тут виникає питання: яку UI бібліотеку обрати? І чи взагалі потрібна бібліотека, чи можна обійтися utility-first CSS на кшталт Tailwind?
У цій главі ми розберемо фундаментальні типи UI рішень, зрозуміємо їх архітектуру, порівняємо підходи та навчимося приймати свідомі рішення.
Щоб зрозуміти, чому ландшафт UI бібліотек виглядає саме так, давайте подивимося, як ми до цього дійшли.
У доволіберативному світі (до React, Vue, Angular) найпопулярнішою UI бібліотекою була jQuery UI.
<!-- jQuery UI: Imperативний підхід -->
<div id="accordion">
<h3>Section 1</h3>
<div>Content 1</div>
<h3>Section 2</h3>
<div>Content 2</div>
</div>
<script>
$(function () {
$('#accordion').accordion()
})
</script>
Проблеми:
З появою React розробники спробували адаптувати Bootstrap:
// react-bootstrap: Обгортка над jQuery плагінами
import { Modal, Button } from 'react-bootstrap'
function MyModal() {
const [show, setShow] = useState(false)
return (
<>
<Button onClick={() => setShow(true)}>Open</Button>
<Modal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton>
<Modal.Title>Modal heading</Modal.Title>
</Modal.Header>
<Modal.Body>Woohoo, you're reading this text in a modal!</Modal.Body>
</Modal>
</>
)
}
Прогрес:
Проблеми:
!important та перезапису CSSПоява повноцінних component libraries для React:
// Material-UI: Все в коробці
import { Button, TextField, Dialog } from '@mui/material'
function MyForm() {
return (
<Dialog open>
<TextField label="Name" variant="outlined" />
<Button variant="contained" color="primary">
Submit
</Button>
</Dialog>
)
}
Плюси:
Мінуси:
Рішення: розділити логіку та стилізацію.
// Radix UI: Headless primitive
import * as Dialog from '@radix-ui/react-dialog'
function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger className="my-button">Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="my-overlay" />
<Dialog.Content className="my-content">
<Dialog.Title>My Dialog</Dialog.Title>
<Dialog.Description>This is a description</Dialog.Description>
<Dialog.Close className="my-close">Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
Революція:
Проблема:
Синтез: Беремо headless UI (Radix) + додаємо готові стилі (Tailwind) + даємо код у власність.
// shadcn/ui: Copy-paste компонент
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
function MyDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
Філософія: Ви не встановлюєте пакунок з NPM. Ви копіюєте код компонента у свій проєкт та можете змінювати його як завгодно.
Давайте систематизуємо те, що існує на ринку. UI рішення можна класифікувати за кількома осями.
Суть: Ви отримуєте компоненти з готовими стилями.
Приклади: Material-UI, Ant Design, Chakra UI, Mantine
Характеристики:
// Все стилі вже є
import { Button } from '@mui/material'
;<Button variant="contained" color="primary">
Click me
</Button>
// Результат: Синя кнопка з Material Design стилем
Переваги:
Недоліки:
Коли використовувати:
Суть: Ви отримуєте логіку, behavior та accessibility — без жодного CSS.
Приклади: Radix UI, Headless UI, React Aria, Ariakit
Характеристики:
// Нуль стилів, тільки логіка
import * as Tabs from '@radix-ui/react-tabs'
;<Tabs.Root defaultValue="tab1">
<Tabs.List className="YOUR_CUSTOM_CLASS">
<Tabs.Trigger value="tab1" className="YOUR_TAB_CLASS">
Tab 1
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1" className="YOUR_CONTENT_CLASS">
Content 1
</Tabs.Content>
</Tabs.Root>
Переваги:
Недоліки:
Коли використовувати:
Суть: Headless UI + Готові стилі на Tailwind + Копіювання коду.
Приклади: shadcn/ui, daisyUI (Tailwind plugin), Flowbite
Характеристики:
# Ви не робите npm install shadcn-ui
# Натомість копіюєте код
npx shadcn@latest add button
# Це створить файл src/components/ui/button.tsx у ВАШОМУ проєкті
// Компонент у ВАШІЙ кодовій базі цілком
import { Button } from '@/components/ui/button'
;<Button variant="destructive" size="lg">
Delete
</Button>
Переваги:
Недоліки:
Коли використовувати:
npm install @mui/material @emotion/react @emotion/styled
-Плюси: Легке оновлення (просто npm update)
node_modulesnpx shadcn@latest add dialog
// tailwind.config.js
plugins: [require('daisyui')]
Розглянемо реальний сценарій: нам потрібно створити Dialog (модальне вікно) з формою.
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField } from '@mui/material'
function MyDialog({ open, onClose }) {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Edit Profile</DialogTitle>
<DialogContent>
<TextField autoFocus margin="dense" label="Email Address" type="email" fullWidth variant="standard" />
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose} variant="contained">
Save
</Button>
</DialogActions>
</Dialog>
)
}
Що отримуємо:
Кастомізація простого кольору кнопки:
// Потрібен ThemeProvider та складний конфіг
import { createTheme, ThemeProvider } from '@mui/material/styles'
const theme = createTheme({
palette: {
primary: {
main: '#your-color',
},
},
})
// Обгорнути весь додаток
;<ThemeProvider theme={theme}>
<MyDialog />
</ThemeProvider>
import * as Dialog from '@radix-ui/react-dialog'
function MyDialog({ open, onClose }) {
return (
<Dialog.Root open={open} onOpenChange={onClose}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 w-[450px]">
<Dialog.Title className="text-lg font-semibold mb-4">Edit Profile</Dialog.Title>
<input
type="email"
className="w-full border border-gray-300 rounded px-3 py-2 mb-4"
placeholder="Email Address"
/>
<div className="flex justify-end gap-2">
<button onClick={onClose} className="px-4 py-2 rounded hover:bg-gray-100">
Cancel
</button>
<button
onClick={onClose}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Save
</button>
</div>
<Dialog.Close className="absolute top-4 right-4">✕</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
Що отримуємо:
Кастомізація кольору кнопки:
// Просто змініть клас
className = 'px-4 py-2 bg-purple-600 text-white rounded'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
function MyDialog({ open, onClose }) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Make changes to your profile here.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">
Email
</Label>
<Input id="email" type="email" className="col-span-3" />
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={onClose}>Save</Button>
</div>
</DialogContent>
</Dialog>
)
}
Що отримуємо:
Кастомізація кольору кнопки:
// Варіант 1: Використати variant
<Button variant="destructive">Delete</Button>
// Варіант 2: Відкрити src/components/ui/button.tsx та додати свій variant
const buttonVariants = cva(
"...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "...",
purple: "bg-purple-600 text-white hover:bg-purple-700", // ← додали
},
},
}
)
// Використати
<Button variant="purple">My Button</Button>
| Критерій | Material-UI | Radix UI | shadcn/ui |
|---|---|---|---|
| Bundle Size (Dialog) | ~89 KB | ~5 KB | ~7 KB |
| Готові стилі | ✅ Так | ❌ Ні | ✅ Так |
| Кастомізація | ⚠️ Складна (theming API) | ✅ Повна (ваш CSS) | ✅ Легка (редагуйте файл) |
| Accessibility | ✅ Так | ✅ Так | ✅ Так (через Radix) |
| Час старту | 🚀 5 хвилин | ⏱️ 30 хвилин | ⏱️ 15 хвилин |
| Learning Curve | Помірна | Низька | Низька |
| Власність коду | ❌ Ні (у node_modules) | ✅ Так (ваші стилі) | ✅ Так (весь компонент) |
| Оновлення | ✅ Автоматичні (npm) | ✅ Автоматичні | ⚠️ Вручну (але ви контролюєте) |
| TypeScript | ✅ Вбудований | ✅ Вбудований | ✅ Вбудований |
| Унікальний дизайн | ❌ Виглядає як MUI | ✅ Повністю ваш | ✅ Базисний + ваш |
Усі сучасні UI бібліотеки акцентують на accessibility, але що це означає насправді?
Accessibility (скорочено a11y: "a" + 11 букв + "y") — це практика створення інтерфейсів,доступних для всіх користувачів, включно з людьми з обмеженими можливостями.
Категорії користувачів:
WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) — стандарт для доступності веб-додатків.
Приклад недоступного Dialog:
// ❌ ПОГАНИЙ приклад:код без accessibility
function BadDialog({ open, onClose, children }) {
if (!open) return null
return (
<div onClick={onClose}>
<div onClick={(e) => e.stopPropagation()}>
{children}
<button onClick={onClose}>×</button>
</div>
</div>
)
}
Проблеми:
aria-labelledby для titleПриклад доступного Dialog (Radix UI):
// ✅ ГАРНИЙ приклад: Radix робить це автоматично
import * as Dialog from '@radix-ui/react-dialog'
;<Dialog.Root open={open} onOpenChange={onClose}>
<Dialog.Portal>
<Dialog.Overlay /> {/* Блокує фон */}
<Dialog.Content>
{' '}
{/* Має role="dialog", aria-modal="true" */}
<Dialog.Title>
{' '}
{/* aria-labelledby="title-id" */}
My Dialog
</Dialog.Title>
<Dialog.Description>
{' '}
{/* aria-describedby="desc-id" */}
Description
</Dialog.Description>
<Dialog.Close /> {/* Закриває на Escape */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Що Radix робить під капотом:
<!-- Згенерований HTML -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="radix-:r0:"
aria-describedby="radix-:r1:"
data-state="open"
tabindex="-1"
style="pointer-events: auto;"
>
<h2 id="radix-:r0:">My Dialog</h2>
<p id="radix-:r1:">Description</p>
</div>
А також JavaScript логіка:
Якщо ви пишете компонент з нуля або оцінюєте бібліотеку, перевірте:
Клавіатурна навігація:
ARIA атрибути:
role (dialog, button, checkbox, menuitem тощо)aria-label або aria-labelledby (опис елемента)aria-describedby (додатковий опис)aria-expanded, aria-selected, aria-checked (стани)aria-hidden для декоративних елементівFocus Management:
Screen Reader Support:
aria-live) для динамічних оновленьЯк приймати свідоме рішення? Задайте собі ці питання:
Пріоритет: Швидкість виходу на ринок
Рекомендація: Material-UI або Chakra UI
Пріоритет: Унікальний дизайн, довгострокове масштабування
Рекомендація: shadcn/ui або Radix UI + Tailwind
Пріоритет: Стабільність, багато функцій з коробки, підтримка
Рекомендація: Ant Design або Material-UI
Пріоритет: Вивчення нових технологій, експерименти
Рекомендація: shadcn/ui
| Якщо використовуєте... | Тоді підходить... |
|---|---|
| Tailwind CSS | shadcn/ui, Headless UI, daisyUI |
| CSS-in-JS (Emotion, Styled Components) | Material-UI, Chakra UI |
| Plain CSS / CSS Modules | Radix UI, Mantine |
| Next.js | shadcn/ui (ідеальна інтеграція), Material-UI |
| Vite + React | Будь-що (усі підтримують) |
Якщо так (публічний продукт, мобільні користувачі):
Якщо ні (internal tools, адмін-панелі):
Повністю кастомний brand:
Базові кольори/шрифти, але стандартні патерни:
Підходить Material Design / Ant Design:
5 хвилин
Material-UI, Chakra UI
npm install @mui/material
# Працює одразу
15-30 хвилин
shadcn/ui
# Настроїти Tailwind
# Запустити shadcn init
# Додати компоненти
1-2 години
Radix UI з нуля
# Настроїти Tailwind
# Написати стилі для кожного компонента
# Створити design system
Розглянемо реальні сценарії:
Задача: Створити dashboard для керування користувачами, аналітики, налаштувань.
Вимоги:
Рішення: Ant Design або Material-UI
Чому:
// Ant Design має готовий Table з усім функціоналом
import { Table, Button, Modal, Form, Input } from 'antd';
const columns = [
{ title: 'Name', dataIndex: 'name', sorter: true, filters: [...] },
{ title: 'Email', dataIndex: 'email' },
{ title: 'Actions', render: (_, record) => <Button>Edit</Button> },
];
<Table
columns={columns}
dataSource={users}
pagination={{ pageSize: 10 }}
onChange={handleTableChange}
/>
Альтернатива: Зі shadcn/ui це потребує інтеграції з TanStack Table + написання логіки сортування/фільтрації.
Задача: Красива посадкова сторінка з анімаціями, формами, hero section.
Вимоги:
Рішення: shadcn/ui або Radix UI + Tailwind
Чому:
Задача: Кросплатформенний мобільний додаток.
Вимоги:
Рішення: Radix UI (веб) або React Native Paper (native)
Чому:
Задача: Створити бібліотеку UI компонентів для спільноти.
Вимоги:
Рішення: Створіть Wrapper над Radix UI (як зробив shadcn)
Чому:
Тепер, коли ви розумієте ландшафт UI бібліотек, ми готові заглибитися в shadcn/ui — рішення, яке поєднує найкраще з обох світів.
У наступній главі ми детально розберемо:
UI Бібліотеки в React
Модуль присвячений вивченню сучасних UI бібліотек для React, з детальним фокусом на shadcn/ui — інноваційний підхід до створення компонентних систем.
Філософія shadcn/ui: "Not a Component Library"
Коли відкриваєш офіційний сайт shadcn/ui, перше, що бачиш — провокаційне твердження: