Тепер, коли ви розумієте філософію shadcn/ui, час перейти від теорії до практики. У цій главі ми встановимо shadcn/ui, налаштуємо проєкт та додамо перші компоненти.
Що ми зробимо:
components.jsonПеред початком переконайтеся, що ви розумієте ці технології:
Якщо ці терміни вам незн айомі, спочатку вивчіть базовий React.
shadcn/ui написаний на TypeScript. Хоча можна використовувати з JavaScript, ви втаратите:
shadcn/ui heavily використовує Tailwind. Ви маєте розуміти:
flex, bg-primary, hover:)sm:, md:, lg:)Якщо ви не знайомі з Tailwind, рекомендую спочатку пройти їхній tutorial.
Перевірте версію:
node -v # v18.0.0 або вище
npm -v # 9.0.0 або вище
shadcn/ui ідеально інтегрується з Next.js (особливо App Router). Це найпростіший варіант.
Запустіть 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
Вибір базового кольору:
Це базовий колір для neutral elements (borders, backgrounds, text). Zinc — універсальний вибір.
components.jsonsrc/lib/utils.ts з функцією cn()globals.css зі змінними теми@/*)Після ініціалізації ваш проєкт має таку структуру:
Розберемо кожен файл:
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"
}
Поля конфігу:
.tsx файли (TypeScript).tw-). Зазвичай порожній.::
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)} />
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
className останнім аргументом в cn(). Це дозволяє користувачам компонента override будь-які стилі.Що це? 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 (простий об'єкт):
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>
)
}
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()Як змінити тему?
Просто відредагуйте CSS variables:
:root {
--primary: oklch(0.5 0.2 280); /* Фіолетовий замість чорного */
}
tailwind.config.ts. Вся конфігурація (кольори, border-radius, dark mode) визначається через CSS variables у globals.css. Tailwind v4 автоматично сканує ваш проєкт та генерує потрібні стилі.Тепер проєкт налаштований. Додаймо кнопку:
npx shadcn@latest add button
Що відбувається:
src/components/ui/button.tsxРезультат:
✔ Installing dependencies.
✔ Created 1 file:
- src/components/ui/button.tsx
Відкрийте src/app/page.tsx:
import { Button } from '@/components/ui/button'
export default function Home() {
return (
<div>
<Button>Click me</Button>
</div>
)
}
npm run dev
Відкрийте http://localhost:3000.
Ви маєте побачити кнопку! 🎉
Якщо ви не використовуєте Next.js, ось як встановити з Vite. Цей варіант використовує Tailwind CSS v4 з сучасним підходом без конфіг-файлів.
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
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'
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.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 з темою.
Аналогічно Next.js:
npx shadcn@latest add button
У 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/uisrc/components/ui/: Тут будуть ВСІ компоненти shadcn/uisrc/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 для підтвердження)
Подивімось на згенерований код кнопки:
// 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} />
Тепер найцікавіше: як змінювати компоненти?
Відкрийте 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' до автокоміта!
Хочете rounded-lg замість rounded-md?
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg ...', // ← змінили md на lg
{
/* ... */
},
)
Усі кнопки тепер мають більший border-radius.
Створіть 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>
Причина: 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/*"]
}
}
}
Причина: Tailwind CSS v4 не підключений як Vite плагін.
Рішення для Vite:
// vite.config.ts
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()], // ← Додайте tailwindcss()
})
Також переконайтесь, що в index.css є:
@import 'tailwindcss';
Причина: Не імпортовано globals.css.
Рішення для Next.js:
// src/app/layout.tsx
import './globals.css' // ← Переконайтесь, що це є
Рішення для Vite:
// src/main.tsx
import './index.css' // ← Тут має бути @import "tailwindcss"
Причина: Клас .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>
)
}
@import "tailwindcss" в CSS, @tailwindcss/vite плагін для Vite)@/components та @/lib)components.json створенийsrc/lib/utils.ts існує з cn() функцієюЯкщо всі пункти виконані — ви готові до використання shadcn/ui! 🎉
У наступній главі ми детально розберемо базові компоненти: Button, Card, Badge, Avatar та інші фундаментальні елементи інтерфейсу.
Філософія shadcn/ui: "Not a Component Library"
Коли відкриваєш офіційний сайт shadcn/ui, перше, що бачиш — провокаційне твердження:
Базові Компоненти shadcn/ui: Фундамент Інтерфейсу
Тепер, коли проєкт налаштований, час розібрати базові компоненти shadcn/ui. Ці компоненти — фундамент будь-якого інтерфейсу. Ви використовуватимете їх щодня: кнопки, картки, бейджі, аватари.