UI Бібліотеки в React

Установка та Налаштування shadcn/ui

Тепер, коли ви розумієте філософію shadcn/ui, час перейти від теорії до практики. У цій главі ми встановимо shadcn/ui, налаштуємо проєкт та додамо перші компоненти.

Установка та Налаштування shadcn/ui

Тепер, коли ви розумієте філософію shadcn/ui, час перейти від теорії до практики. У цій главі ми встановимо shadcn/ui, налаштуємо проєкт та додамо перші компоненти.

Що ми зробимо:

  • Встановимо shadcn/ui у Next.js проєкт
  • Альтернативно: встановимо у Vite + React проєкт
  • Розберемо кожен крок установки
  • Налаштуємо components.json
  • Додамо перший компонент
  • Навчимося troubleshooting типових проблем
Важливо: shadcn/ui — це не пакунок з NPM. Ми будемо копіювати компоненти у ваш проєкт. Тому prerequisites критичні — без них нічого не запрацює.

Prerequisites (Що треба знати заздалегідь)

Перед початком переконайтеся, що ви розумієте ці технології:

1. React Fundamentals

  • Components, Props, State
  • Hooks (useState, useRef, useEffect)
  • Event handling

Якщо ці терміни вам незн айомі, спочатку вивчіть базовий React.

2. TypeScript (Рекомендовано)

shadcn/ui написаний на TypeScript. Хоча можна використовувати з JavaScript, ви втаратите:

  • Type safety
  • Автокомпліт у VSCode
  • Безпеку рефакторингу

3. Tailwind CSS

shadcn/ui heavily використовує Tailwind. Ви маєте розуміти:

  • Utility classes (flex, bg-primary, hover:)
  • Responsive prefixes (sm:, md:, lg:)
  • Базову концепцію

Якщо ви не знайомі з Tailwind, рекомендую спочатку пройти їхній tutorial.

4. Node.js та Package Manager

  • Node.js v18+ встановлений
  • npm, pnpm або yarn

Перевірте версію:

node -v  # v18.0.0 або вище
npm -v   # 9.0.0 або вище

Варіант 1: Установка з Next.js (Рекомендовано)

shadcn/ui ідеально інтегрується з Next.js (особливо App Router). Це найпростіший варіант.

Крок 1: Створення Проєкту та Ініціалізація shadcn/ui

Запустіть init команду — вона створить Next.js проєкт і налаштує shadcn/ui автоматично:

npx shadcn@latest init

CLI запитає, який тип проєкту створити (Next.js або Monorepo), а потім задасть питання про налаштування:

✔ Which color would you like to use as the base color? › Zinc

Вибір базового кольору:

  • Slate, Gray, Zinc, Neutral, Stone

Це базовий колір для neutral elements (borders, backgrounds, text). Zinc — універсальний вибір.

Що відбувається під капотом? CLI автоматично:
  1. Створює Next.js проєкт з TypeScript та Tailwind CSS
  2. Налаштовує components.json
  3. Створює src/lib/utils.ts з функцією cn()
  4. Оновлює globals.css зі змінними теми
  5. Налаштовує path aliases (@/*)

Крок 2: Що створив CLI?

Після ініціалізації ваш проєкт має таку структуру:

Розберемо кожен файл:

components.json (Конфігураційний файл)

{
    "$schema": "https://ui.shadcn.com/schema.json",
    "style": "new-york",
    "rsc": true,
    "tsx": true,
    "tailwind": {
        "config": "",
        "css": "app/globals.css",
        "baseColor": "zinc",
        "cssVariables": true,
        "prefix": ""
    },
    "aliases": {
        "components": "@/components",
        "utils": "@/lib/utils",
        "ui": "@/components/ui",
        "lib": "@/lib",
        "hooks": "@/hooks"
    },
    "iconLibrary": "lucide"
}

Поля конфігу:

style
'new-york'
Візуальний стиль компонентів (New York — сучасний sharp дизайн).
rsc
boolean
React Server Components підтримка (Next.js App Router).
tsx
boolean
Чи генерувати .tsx файли (TypeScript).
tailwind.config
string
Шлях до конфігурації Tailwind (порожній для Tailwind CSS v4, де конфіг не потрібен).
tailwind.css
string
Шлях до глобального CSS файлу.
tailwind.baseColor
'slate' | 'gray' | 'zinc' | 'neutral' | 'stone'
Базовий колір для neutral елементів.
tailwind.cssVariables
boolean
Використовувати CSS variables для кольорів.
tailwind.prefix
string
Prefix для Tailwind класів (наприклад, tw-). Зазвичай порожній.
aliases
object
Path aliases для import statements. Критично важливе — CLI використовує це для генерації imports.
iconLibrary
'lucide' | 'custom'
Бібліотека іконок. Lucide — дефолт (React іконки від творців Feather Icons).

::

src/lib/utils.ts (Utility функція)

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs))
}

Що робить cn() функція?

Проблема: Tailwind класи можуть конфліктувати:

// Який колір буде?
<div className="bg-red-500 bg-blue-500" />
// Відповідь: залежить від порядку в згенерованому CSS (непередбачувано!)

Рішення: twMerge вирішує конфлікти:

cn('bg-red-500', 'bg-blue-500')
// Результат: "bg-blue-500" (останній виграє)

А clsx? Зручний синтаксис для conditional classes:

cn('button', isActive && 'active', isPrimary ? 'primary' : 'secondary', className)

Використання в компонентах:

<Button className={cn('base-classes', variant === 'default' && 'default-classes', className)} />
Важливо: Всі shadcn/ui компоненти використовують cn(). Не видаляйте цей файл!
Детальний Розбір: Як Працює cn()

Розберемо цю функцію по частинах, бо вона критична для розуміння shadcn/ui.

Проблема 1: Конфлікти Tailwind класів

// Уявіть, що ви передаєте кастомні класи в компонент:
<Button className="bg-red-500">Delete</Button>

// Але компонент вже має базовий bg-primary
// Який колір буде в результаті?

Відповідь: непередбачувано! В Tailwind CSS, коли є два класи що встановлюють одну і ту саму властивість (наприклад, bg-red-500 та bg-primary), результат залежить від порядку в згенерованому CSS файлі, а не від порядку в HTML.

Рішення: tailwind-merge

import { twMerge } from 'tailwind-merge'

twMerge('bg-primary', 'bg-red-500')
// Результат: "bg-red-500" (останній виграє)

twMerge('px-4 py-2', 'px-8')
// Результат: "py-2 px-8" (px-4 видалено, px-8 залишився)

tailwind-merge інтелектуально визначає конфлікти та залишає тільки останнє значення для кожної CSS властивості.

Проблема 2: Conditional класи

// Незручний спосіб:
<div className={`button ${isActive ? 'active' : ''} ${isPrimary ? 'primary' : 'secondary'} ${className || ''}`} />

// Треба стежити за пробілами, обробляти undefined

Рішення: clsx

import { clsx } from 'clsx'

clsx('button', isActive && 'active', isPrimary ? 'primary' : 'secondary', className)
// Автоматично:
// - Фільтрує false/null/undefined
// - Додає пробіли
// - Об'єднує все в рядок

Підтримувані синтаксиси clsx:

// Рядки
clsx('foo', 'bar') // 'foo bar'

// Об'єкти (ключ додається якщо значення truthy)
clsx({ foo: true, bar: false, baz: 1 }) // 'foo baz'

// Масиви
clsx(['foo', 'bar']) // 'foo bar'

// Змішане
clsx('button', { active: isActive }, [isPrimary && 'primary']) // 'button active primary'

// Undefined/null/false ігноруються
clsx('foo', null, undefined, false, 'bar') // 'foo bar'

Комбінація: cn() = clsx() + twMerge()

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs))
}

// Використання:
cn('bg-primary px-4', 'bg-red-500', isActive && 'active')
// Результат: "px-4 bg-red-500 active"
// ↑ clsx обробив conditional та об'єднав
// ↑ twMerge вирішив конфлікт bg-primary vs bg-red-500

Практичний приклад в компоненті:

// src/components/ui/button.tsx
const Button = ({ className, variant, ...props }) => {
    return (
        <button
            className={cn(
                // Базові класи (завжди присутні)
                'inline-flex items-center justify-center rounded-md',
                // Варіанти (з CVA)
                buttonVariants({ variant }),
                // Кастомні класи від користувача (можуть override)
                className,
            )}
            {...props}
        />
    )
}

// Використання:
;<Button variant="primary" className="bg-green-500">
    Save
</Button>
// Результат: bg-green-500 виграє над bg-primary
Best Practice: Завжди передавайте className останнім аргументом в cn(). Це дозволяє користувачам компонента override будь-які стилі.
CVA (Class Variance Authority): Керування Варіантами

Що це? CVA — бібліотека для створення компонентів з варіантами (variants). Вона автоматично генерує TypeScript типи та обробляє комбінації класів.

Встановлення (уже включено при npx shadcn@latest init):

npm install class-variance-authority

Базовий синтаксис:

import { cva } from 'class-variance-authority'

const buttonVariants = cva(
    // Базові класи (завжди присутні)
    'inline-flex items-center justify-center rounded-md transition-colors',
    {
        variants: {
            // Варіант 1: колір
            variant: {
                default: 'bg-primary text-white',
                destructive: 'bg-red-600 text-white',
                outline: 'border border-gray-300',
            },
            // Варіант 2: розмір
            size: {
                sm: 'h-8 px-3 text-xs',
                md: 'h-10 px-4 text-sm',
                lg: 'h-12 px-6 text-base',
            },
        },
        defaultVariants: {
            variant: 'default',
            size: 'md',
        },
    },
)

// Результат: функція яка приймає варіанти та повертає класи
buttonVariants({ variant: 'destructive', size: 'lg' })
// → "inline-flex items-center justify-center rounded-md transition-colors bg-red-600 text-white h-12 px-6 text-base"

TypeScript інтеграція:

import { type VariantProps } from 'class-variance-authority'

// Автоматично генерує тип з можливими варіантами
type ButtonVariants = VariantProps<typeof buttonVariants>
// Тип:
// {
//   variant?: "default" | "destructive" | "outline"
//   size?: "sm" | "md" | "lg"
// }

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, ButtonVariants {
    // variant та size автоматично додаються з правильними типами!
}

const Button = ({ variant, size, className, ...props }: ButtonProps) => {
    return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}

// TypeScript автокомпліт:
<Button variant="destructive" size="lg" /> // ✅
<Button variant="invalid" /> // ❌ TypeScript error!

Compound Variants (Комбіновані варіанти):

Іноді потрібна спеціальна стилізація для певної комбінації варіантів:

const buttonVariants = cva('...', {
    variants: {
        variant: {
            default: 'bg-primary',
            outline: 'border',
        },
        size: {
            sm: 'h-8',
            lg: 'h-12',
        },
    },
    compoundVariants: [
        {
            // Коли variant="outline" І size="lg"
            variant: 'outline',
            size: 'lg',
            // Додати ці класи:
            className: 'border-2', // Товстіша рамка для великих outline кнопок
        },
    ],
})

Приклад з усіма фічами:

const alertVariants = cva(
    // Базові класи
    'relative w-full rounded-lg border px-4 py-3',
    {
        variants: {
            variant: {
                default: 'bg-background text-foreground',
                destructive: 'border-red-500/50 text-red-600 dark:border-red-500 [&>svg]:text-red-600',
            },
        },
        defaultVariants: {
            variant: 'default',
        },
    },
)

// Використання в компоненті:
interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {}

const Alert = ({ variant, className, ...props }: AlertProps) => (
    <div role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
)
Чому CVA, а не просто об'єкт?
// Без CVA (простий об'єкт):
const variants = {
  primary: "bg-primary",
  secondary: "bg-secondary"
}
<button className={variants[variant]} /> // Працює, але:
// ❌ Немає TypeScript автокомпліту
// ❌ Немає compound variants
// ❌ Незручно комбінувати кілька варіантів
// ❌ Немає автоматичної генерації типів

// З CVA:
// ✅ Повна TypeScript підтримка
// ✅ Compound variants
// ✅ Легко комбінувати варіанти
// ✅ Автоматична генерація VariantProps

Реальний приклад: Кастомний Badge компонент:

// src/components/ui/badge.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const badgeVariants = cva(
    'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors',
    {
        variants: {
            variant: {
                default: 'border-transparent bg-primary text-primary-foreground',
                secondary: 'border-transparent bg-secondary text-secondary-foreground',
                destructive: 'border-transparent bg-destructive text-destructive-foreground',
                outline: 'text-foreground',
                success: 'border-transparent bg-green-500 text-white',
                warning: 'border-transparent bg-yellow-500 text-white',
            },
            size: {
                sm: 'px-2 py-0.5 text-xs',
                md: 'px-2.5 py-0.5 text-sm',
                lg: 'px-3 py-1 text-base',
            },
        },
        compoundVariants: [
            {
                variant: 'outline',
                size: 'lg',
                className: 'border-2',
            },
        ],
        defaultVariants: {
            variant: 'default',
            size: 'md',
        },
    },
)

export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, size, ...props }: BadgeProps) {
    return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />
}

export { Badge, badgeVariants }

Використання:

import { Badge } from '@/components/ui/badge'

function OrderStatus() {
    return (
        <div className="flex gap-2">
            <Badge variant="success">Completed</Badge>
            <Badge variant="warning" size="lg">
                Pending
            </Badge>
            <Badge variant="destructive">Cancelled</Badge>
            <Badge variant="outline" className="border-blue-500">
                Custom
            </Badge>
        </div>
    )
}
Коли використовувати CVA:Використовуйте CVA коли:
  • Компонент має 2+ варіанти (колір, розмір, стан)
  • Потрібна TypeScript безпека для props
  • Є compound variants (спеціальні комбінації)
Не потрібен CVA коли:
  • Компонент має тільки один вигляд
  • Стилі повністю динамічні (приходять ззовні)
  • Простий wrapper без варіацій

globals.css (Стилі теми — Tailwind CSS v4)

CLI налаштовує ваш globals.css з OKLCH кольорами (сучасний формат кольорів):

@import 'tailwindcss';

:root {
    --radius: 0.625rem;
    --background: oklch(1 0 0);
    --foreground: oklch(0.141 0.005 285.823);
    --card: oklch(1 0 0);
    --card-foreground: oklch(0.141 0.005 285.823);
    --popover: oklch(1 0 0);
    --popover-foreground: oklch(0.141 0.005 285.823);
    --primary: oklch(0.21 0.006 285.885);
    --primary-foreground: oklch(0.985 0 0);
    --secondary: oklch(0.967 0.001 286.375);
    --secondary-foreground: oklch(0.21 0.006 285.885);
    --muted: oklch(0.967 0.001 286.375);
    --muted-foreground: oklch(0.552 0.016 285.938);
    --accent: oklch(0.967 0.001 286.375);
    --accent-foreground: oklch(0.21 0.006 285.885);
    --destructive: oklch(0.577 0.245 27.325);
    --destructive-foreground: oklch(0.985 0 0);
    --border: oklch(0.92 0.004 286.32);
    --input: oklch(0.92 0.004 286.32);
    --ring: oklch(0.705 0.015 286.067);
}

.dark {
    --background: oklch(0.141 0.005 285.823);
    --foreground: oklch(0.985 0 0);
    --primary: oklch(0.92 0.004 286.32);
    --primary-foreground: oklch(0.21 0.006 285.885);
    /* ... інші dark mode кольори */
}
Чому OKLCH замість HSL? Tailwind CSS v4 та shadcn/ui перейшли на формат OKLCH (Oklab Lightness Chroma Hue). Цей формат:
  • Перцептуально рівномірний — однакові числові зміни дають однакові візуальні зміни
  • Ширший gamut — підтримує P3 та інші сучасні кольоровие простори
  • Нативна підтримка — всі сучасні браузери підтримують oklch()

Як змінити тему?

Просто відредагуйте CSS variables:

:root {
    --primary: oklch(0.5 0.2 280); /* Фіолетовий замість чорного */
}
Важливо: В Tailwind CSS v4 більше не потрібен файл tailwind.config.ts. Вся конфігурація (кольори, border-radius, dark mode) визначається через CSS variables у globals.css. Tailwind v4 автоматично сканує ваш проєкт та генерує потрібні стилі.

Крок 3: Додавання Першого Компонента

Тепер проєкт налаштований. Додаймо кнопку:

npx shadcn@latest add button

Що відбувається:

  1. CLI скачує код компонента з registry
  2. Створює src/components/ui/button.tsx
  3. Автоматично встановлює залежності (якщо потрібні)

Результат:

✔ Installing dependencies.
✔ Created 1 file:
  - src/components/ui/button.tsx

Крок 4: Використання Компонента

Відкрийте src/app/page.tsx:

import { Button } from '@/components/ui/button'

export default function Home() {
    return (
        <div>
            <Button>Click me</Button>
        </div>
    )
}

Крок 5: Запуск Dev Server

npm run dev

Відкрийте http://localhost:3000.

Ви маєте побачити кнопку! 🎉

Варіант 2: Установка з Vite + React

Якщо ви не використовуєте Next.js, ось як встановити з Vite. Цей варіант використовує Tailwind CSS v4 з сучасним підходом без конфіг-файлів.

Крок 1: Створення Vite Проєкту

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

Крок 2: Установка Tailwind CSS v4

Tailwind CSS v4 використовує Vite плагін замість PostCSS:

npm install tailwindcss @tailwindcss/vite
Увага: Це відрізняється від старого підходу! Більше не потрібніpostcss, autoprefixer та npx tailwindcss init.

Замініть вміст src/index.css:

@import 'tailwindcss';

Переконайтесь, що src/main.tsx імпортує CSS:

import './index.css'

Крок 3: Налаштування Path Aliases

Vite не підтримує @/* aliases з коробки. Потрібна конфігурація.

1. Встановіть types для node:

npm install -D @types/node

2. Оновіть tsconfig.json:

Сучасний Vite розділяє TypeScript конфігурацію на три файли. Додайте baseUrl та paths у tsconfig.json:

{
    "files": [],
    "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["./src/*"]
        }
    }
}

3. Оновіть tsconfig.app.json:

{
    "compilerOptions": {
        // ... інші опції
        "baseUrl": ".",
        "paths": {
            "@/*": ["./src/*"]
        }
    }
}

4. Оновіть vite.config.ts:

import path from 'path'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
    plugins: [react(), tailwindcss()],
    resolve: {
        alias: {
            '@': path.resolve(__dirname, './src'),
        },
    },
})
Зверніть увагу: tailwindcss() додається як Vite плагін, а не через PostCSS. Це забезпечує кращу продуктивність та DX.

Крок 4: Ініціалізація shadcn/ui

npx shadcn@latest init

CLI автоматично визначить Vite та Tailwind CSS:

✔ Which color would you like to use as the base color? › Neutral

CLI створить components.json, src/lib/utils.ts та оновить CSS з темою.

Крок 5: Додавання Компонентів

Аналогічно Next.js:

npx shadcn@latest add button

Крок 6: Використання

У src/App.tsx:

import { Button } from '@/components/ui/button'

function App() {
    return (
        <div className="flex min-h-svh flex-col items-center justify-center">
            <Button>Click me</Button>
        </div>
    )
}

export default App

Структура Проєкту після Установки

Після setup ваш проєкт має таку структуру:

Пояснення:

  • components.json: Конфігурація shadcn/ui
  • src/components/ui/: Тут будуть ВСІ компоненти shadcn/ui
  • src/lib/utils.ts: Utility функції (основна — cn())
Важливо: Компоненти у src/components/ui/ — це ваш код. Ви можете редагувати їх як завгодно. Це не node_modules.

Додавання Кількох Компонентів Одразу

Можна додати кілька компонентів однією командою:

npx shadcn@latest add button card dialog input label

Або додати все, що може знадобитися (⚠️ багато файлів):

npx shadcn@latest add
# CLI покаже список всіх доступних компонентів
# Оберіть потрібні (Space для вибору, Enter для підтвердження)

Огляд Компонента: Button

Подивімось на згенерований код кнопки:

// src/components/ui/button.tsx
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

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-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
    {
        variants: {
            variant: {
                default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
                destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
                outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
                secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
                ghost: 'hover:bg-accent hover:text-accent-foreground',
                link: 'text-primary underline-offset-4 hover:underline',
            },
            size: {
                default: 'h-9 px-4 py-2',
                sm: 'h-8 rounded-md px-3 text-xs',
                lg: 'h-10 rounded-md px-8',
                icon: 'h-9 w-9',
            },
        },
        defaultVariants: {
            variant: 'default',
            size: 'default',
        },
    },
)

export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
    asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
    ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : 'button'
        return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
    },
)
Button.displayName = 'Button'

export { Button, buttonVariants }

Розбір коду:

1. CVA для variants:

const buttonVariants = cva(/* базові класи */, {
  variants: { /* варіації */ },
  defaultVariants: { /* дефолти */ }
})

2. TypeScript типи:

export interface ButtonProps
    extends
        React.ButtonHTMLAttributes<HTMLButtonElement>, // Усі HTML button props
        VariantProps<typeof buttonVariants> {
    // variant та size props
    asChild?: boolean // Radix Slot API
}

3. Radix Slot (опціонально):

<Button asChild>
    <a href="/home">Home</a>
</Button>
// Рендерить <a> з стилями кнопки, а не <button>

4. forwardRef для ref передачі:

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(...)
// Можна використати ref:
const buttonRef = useRef<HTMLButtonElement>(null)
<Button ref={buttonRef} />

Кастомізація Компонента

Тепер найцікавіше: як змінювати компоненти?

Приклад 1: Додати новий variant

Відкрийте button.tsx та додайте:

const buttonVariants = cva('...', {
    variants: {
        variant: {
            default: '...',
            destructive: '...',
            // ↓ Додаємо новий variant
            success: 'bg-green-600 text-white shadow-sm hover:bg-green-700',
        },
    },
})

Використання:

<Button variant="success">Save</Button>

TypeScript автоматично додасть 'success' до автокоміта!

Приклад 2: Змінити базовий стиль

Хочете rounded-lg замість rounded-md?

const buttonVariants = cva(
    'inline-flex items-center justify-center rounded-lg ...', // ← змінили md на lg
    {
        /* ... */
    },
)

Усі кнопки тепер мають більший border-radius.

Приклад 3: Додати іконку

Створіть wrapper компонент:

// src/components/icon-button.tsx
import { Button, ButtonProps } from '@/components/ui/button'
import { Loader2 } from 'lucide-react'

interface IconButtonProps extends ButtonProps {
    icon?: React.ReactNode
    loading?: boolean
}

export function IconButton({ icon, loading, children, ...props }: IconButtonProps) {
    return (
        <Button disabled={loading || props.disabled} {...props}>
            {loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : icon && <span className="mr-2">{icon}</span>}
            {children}
        </Button>
    )
}

Використання:

import { Save } from 'lucide-react'
;<IconButton icon={<Save />} loading={isSubmitting}>
    Save Changes
</IconButton>

Troubleshooting: Типові Проблеми

Проблема 1: "Cannot find module '@/components/ui/button'"

Причина: Path aliases не налаштовані.

Рішення для Vite:

// vite.config.ts
import path from "path"

export default define Config({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
// tsconfig.json
{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["./src/*"]
        }
    }
}

Проблема 2: Tailwind класи не застосовуються

Причина: Tailwind CSS v4 не підключений як Vite плагін.

Рішення для Vite:

// vite.config.ts
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
    plugins: [react(), tailwindcss()], // ← Додайте tailwindcss()
})

Також переконайтесь, що в index.css є:

@import 'tailwindcss';

Проблема 3: CSS variables не працюють

Причина: Не імпортовано globals.css.

Рішення для Next.js:

// src/app/layout.tsx
import './globals.css' // ← Переконайтесь, що це є

Рішення для Vite:

// src/main.tsx
import './index.css' // ← Тут має бути @import "tailwindcss"

Проблема 4: Dark mode не працює

Причина: Клас .dark не додається на <html>.

Рішення для Next.js (використайте next-themes):

npm install next-themes
// src/app/layout.tsx
import { ThemeProvider } from 'next-themes'

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en" suppressHydrationWarning>
            <body>
                <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
                    {children}
                </ThemeProvider>
            </body>
        </html>
    )
}

Theme toggle компонент:

'use client'

import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'

export function ThemeToggle() {
    const { setTheme, theme } = useTheme()

    return (
        <Button variant="outline" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
            Toggle theme
        </Button>
    )
}

Підсумок: Checklist Успішної Установки

  • Node.js v18+ встановлений
  • Tailwind CSS v4 налаштований (@import "tailwindcss" в CSS, @tailwindcss/vite плагін для Vite)
  • Path aliases працюють (@/components та @/lib)
  • components.json створений
  • src/lib/utils.ts існує з cn() функцією
  • CSS файл містить OKLCH CSS variables для теми
  • Перший компонент (Button) доданий та працює
  • Dark mode налаштований (опціонально)

Якщо всі пункти виконані — ви готові до використання shadcn/ui! 🎉

У наступній главі ми детально розберемо базові компоненти: Button, Card, Badge, Avatar та інші фундаментальні елементи інтерфейсу.

Далі: Базові Компоненти →

Copyright © 2026