Базові Компоненти shadcn/ui: Фундамент Інтерфейсу
Базові Компоненти shadcn/ui: Фундамент Інтерфейсу
Тепер, коли проєкт налаштований, час розібрати базові компоненти shadcn/ui. Ці компоненти — фундамент будь-якого інтерфейсу. Ви використовуватимете їх щодня: кнопки, картки, бейджі, аватари.
У цій главі ми не просто покажемо, як використовувати компоненти. Ми розберемо:
- Як працює кожен компонент під капотом
- Чому саме така структура
- Як адаптувати під свої потреби
- Accessibility особливості
Button: Серце Інтерфейсу
Кнопка — найпростіший і найчастіше використовуваний компонент. Але не дайте простоті вас обдурити. Добра кнопка — це:
- Візуальний feedback (hover, active, focus, disabled states)
- Accessibility (keyboard navigation, screen reader support)
- Гнучкість (варіанти, розміри, іконки)
- Performance (мемоізація, оптимізовані transitions)
Додавання Компонента
npx shadcn@latest add button
Це створить src/components/ui/button.tsx.
Анатомія 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',
{
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. Базові Класи (Завжди Присутні)
'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'
Розберемо кожен клас:
inline-flex items-center justify-center: Flexbox для центрування контенту (текст + іконка)gap-2: Відступ між іконкою та текстом (8px)whitespace-nowrap: Текст не переноситься (кнопка не розтягується на 2 рядки)rounded-md: Border radius (6px за замовчуванням)text-sm font-medium: Розмір шрифту 14px, вага 500transition-colors: Плавна зміна кольору (200ms)focus-visible:outline-none focus-visible:ring-1: Accessibility — показує focus ring тільки при клавіатурній навігаціїdisabled:pointer-events-none disabled:opacity-50: Disabled стан
focus-visible, а не focus?focus спрацьовує завжди (навіть при кліку мишею).focus-visible спрацьовує тільки при клавіатурній навігації (Tab).Це краще UX — користувач миші не бачить зайве outline, але screen reader користувач бачить її.2. Variants (Варіанти Кольору)
default — Primary кнопка:
'bg-primary text-primary-foreground shadow hover:bg-primary/90'
bg-primary: Основний колір (темний)text-primary-foreground: Контрастний текст (білий)shadow: Легка тіньhover:bg-primary/90: Темніший на 10% при наведенні
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'
Прозорий фон, при hover — легкий accent.
ghost — Мінімалістична кнопка:
'hover:bg-accent hover:text-accent-foreground'
Без фону, з'являється тільки при hover.
link — Стилізація як текстове посилання:
'text-primary underline-offset-4 hover:underline'
3. Sizes (Розміри)
size: {
default: "h-9 px-4 py-2", // 36px висота
sm: "h-8 rounded-md px-3 text-xs", // 32px висота
lg: "h-10 rounded-md px-8", // 40px висота
icon: "h-9 w-9", // Квадратна кнопка для іконки
}
4. Radix Slot API
const Comp = asChild ? Slot : 'button'
Що це означає?
asChild дозволяє рендерити будь-який елемент зі стилями кнопки:
// Звичайна кнопка
<Button>Click me</Button>
// → <button>Click me</button>
// Кнопка як посилання
<Button asChild>
<a href="/home">Home</a>
</Button>
// → <a href="/home" class="button-styles">Home</a>
Це корисно для:
- SEO (посилання як кнопки)
- React Router
<Link> - Next.js
<Link>
Використання: Всі Варіанти
import { Button } from '@/components/ui/button'
import { Mail, Loader2 } from 'lucide-react'
export function ButtonDemo() {
return (
<div className="flex flex-col gap-4">
{/* Варіанти */}
<div className="flex gap-2">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
{/* Розміри */}
<div className="flex items-end gap-2">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<Mail className="h-4 w-4" />
</Button>
</div>
{/* З іконками */}
<div className="flex gap-2">
<Button>
<Mail className="mr-2 h-4 w-4" />
Login with Email
</Button>
<Button variant="outline">
Send
<Mail className="ml-2 h-4 w-4" />
</Button>
</div>
{/* Loading state */}
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
{/* asChild приклад */}
<Button asChild>
<a href="https://github.com">GitHub</a>
</Button>
</div>
)
}

Кастомізація: Додавання Власних Варіантів
Відкрийте button.tsx та додайте:
const buttonVariants = cva('...', {
variants: {
variant: {
default: '...',
destructive: '...',
outline: '...',
secondary: '...',
ghost: '...',
link: '...',
success: 'bg-green-600 text-white shadow hover:bg-green-700',
gradient: 'bg-gradient-to-r from-purple-600 to-pink-600 text-white',
},
size: {
/* ... */
},
},
})
TypeScript автоматично додасть 'success' та 'gradient' до автокомпліту!
<Button variant="success">Save</Button>
<Button variant="gradient">Special Offer</Button>
Accessibility Checklist
- Keyboard accessible: Enter і Space активують кнопку
- Focus visible:
focus-visible:ringпоказує outline - Disabled state:
disabled:pointer-events-noneблокує взаємодію - Screen reader: Кнопка має семантичний
<button>тег (якщо неasChild)
asChild з <a>, не забудьте role="button":<Button asChild>
<a href="/action" role="button">
Non-navigation Action
</a>
</Button>
<a> для навігації, а <button> для дій.Card: Контейнер Контенту
Card — універсальний компонент для групування пов'язаного контенту. Подумайте про картки товарів, профілі користувачів, статті блогу.
Додавання
npx shadcn@latest add card
Анатомія Card
shadcn/ui використовує Compound Components pattern:
// src/components/ui/card.tsx
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-xl border bg-card text-card-foreground shadow', className)} {...props} />
))
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
)
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
),
)
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
),
)
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
)
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
)
Compound Components Pattern — це коли компонент складається з кількох під-компонентів:
<Card> {/* Контейнер */}
<CardHeader> {/* Шапка */}
<CardTitle> {/* Заголовок */}
<CardDescription> {/* Опис */}
</CardHeader>
<CardContent> {/* Основний контент */}
<CardFooter> {/* Футер (кнопки, дії) */}
</Card>
Переваги:
- Гнучка композиція (можна пропустити будь-яку частину)
- Семантична структура
- Легко стилізувати окремі частини
Використання: Картка Проєкту
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
export function ProjectCard() {
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>Create Project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="framework">Framework</Label>
<select id="framework" className="...">
<option>Next.js</option>
<option>Vite</option>
</select>
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
)
}

Варіанти Використання
1. Картка Профілю
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage src="/avatar.jpg" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<div>
<CardTitle>John Doe</CardTitle>
<CardDescription>@johndoe</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">Full-stack developer passionate about React and TypeScript.</p>
</CardContent>
<CardFooter>
<Button className="w-full">Follow</Button>
</CardFooter>
</Card>
2. Картка Статистики (без футера)
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
3. Grid Карток
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map((product) => (
<Card key={product.id}>
<CardHeader>
<img src={product.image} alt={product.name} className="aspect-square object-cover rounded-md" />
</CardHeader>
<CardContent>
<CardTitle>{product.name}</CardTitle>
<CardDescription>${product.price}</CardDescription>
</CardContent>
<CardFooter>
<Button className="w-full">Add to Cart</Button>
</CardFooter>
</Card>
))}
</div>
Кастомізація
Додайте hover effect:
<Card className="hover:shadow-lg transition-shadow cursor-pointer">{/* ... */}</Card>
Зміна border radius:
<Card className="rounded-2xl">
{' '}
{/* Більш округлі кути */}
{/* ... */}
</Card>
Badge: Індикатори Стану
Badge — маленький індикатор для статусів, категорій, лейблів.
Додавання
npx shadcn@latest add badge
Код Компонента
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
Використання
import { Badge } from '@/components/ui/badge'
export function OrderStatus() {
return (
<div className="flex gap-2">
<Badge>New</Badge>
<Badge variant="secondary">In Progress</Badge>
<Badge variant="destructive">Cancelled</Badge>
<Badge variant="outline">Draft</Badge>
</div>
)
}

Реальні Сценарії
Статуси Замовлень
function OrderBadge({ status }: { status: string }) {
const variants = {
pending: 'secondary',
processing: 'default',
shipped: 'default',
delivered: 'default',
cancelled: 'destructive',
} as const
return <Badge variant={variants[status]}>{status.charAt(0).toUpperCase() + status.slice(1)}</Badge>
}
Категорії Блогу
<div className="flex gap-2">
{post.tags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
Нотифікації
<button className="relative">
<Bell className="h-5 w-5" />
<Badge className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0">3</Badge>
</button>
Avatar: Представлення Користувача
Avatar — компонент для відображення зображення користувача з fallback.
Додавання
npx shadcn@latest add avatar
Анатомія
// Використовує @radix-ui/react-avatar
import * as AvatarPrimitive from '@radix-ui/react-avatar'
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
))
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} {...props} />
))
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)}
{...props}
/>
))
Radix Avatar автоматично:
- Показує fallback, поки зображення завантажується
- Показує fallback, якщо зображення не завантажилось
- Підтримує lazy loading
Використання
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
export function UserAvatar() {
return (
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
)
}
Як це працює:
- Radix намагається завантажити зображення
- Поки завантажується → показує
AvatarFallback(CN) - Якщо завантажилось → показує
AvatarImage - Якщо помилка завантаження → залишається
AvatarFallback
![]()
Варіанти Розмірів
// Маленький (32px)
<Avatar className="h-8 w-8">
<AvatarImage src="/avatar.jpg" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
// Дефолт (40px)
<Avatar>
<AvatarImage src="/avatar.jpg" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
// Великий (64px)
<Avatar className="h-16 w-16">
<AvatarImage src="/avatar.jpg" />
<AvatarFallback className="text-lg">JD</AvatarFallback>
</Avatar>
Реальні Сценарії
Список Коментарів
{
comments.map((comment) => (
<div key={comment.id} className="flex gap-4">
<Avatar>
<AvatarImage src={comment.user.avatar} />
<AvatarFallback>{comment.user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">{comment.user.name}</p>
<p className="text-sm text-muted-foreground">{comment.text}</p>
</div>
</div>
))
}
Online Status Indicator
<div className="relative">
<Avatar>
<AvatarImage src="/avatar.jpg" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<span className="absolute bottom-0 right-0 block h-3 w-3 rounded-full bg-green-500 ring-2 ring-white" />
</div>
Separator: Візуальне Розділення
Separator — тонка лінія для візуального розділення контенту.
Додавання
npx shadcn@latest add separator
Використання
import { Separator } from '@/components/ui/separator'
export function SeparatorDemo() {
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-medium">Profile</h3>
<p className="text-sm text-muted-foreground">Manage your account settings</p>
</div>
<Separator />
<div>
<h3 className="text-lg font-medium">Security</h3>
<p className="text-sm text-muted-foreground">Update your password</p>
</div>
</div>
)
}
Вертикальний Separator
<div className="flex h-5 items-center space-x-4">
<div>Blog</div>
<Separator orientation="vertical" />
<div>Docs</div>
<Separator orientation="vertical" />
<div>About</div>
</div>
Skeleton: Loading Placeholder
Skeleton — placeholder для контенту, що завантажується.
Додавання
npx shadcn@latest add skeleton
Код
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('animate-pulse rounded-md bg-primary/10', className)} {...props} />
}
Використання
import { Skeleton } from '@/components/ui/skeleton'
export function SkeletonCard() {
return (
<Card>
<CardHeader>
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</CardHeader>
<CardContent>
<Skeleton className="h-[200px] w-full" />
</CardContent>
</Card>
)
}
Реальний Приклад: Profile Loading
function ProfileSkeleton() {
return (
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)
}
// Використання з даними
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId)
if (isLoading) return <ProfileSkeleton />
return (
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.name.slice(0, 2)}</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
)
}

Підсумок: Фундамент Готовий
Ми розібрали базові компоненти shadcn/ui:
| Компонент | Призначення | Ключові Features |
|---|---|---|
| Button | Дії користувача | Variants, sizes, asChild API, accessibility |
| Card | Групування контенту | Compound components, гнучка композиція |
| Badge | Статуси та лейбли | Маленький, яскравий індикатор |
| Avatar | Зображення користувача | Automatic fallback, lazy loading |
| Separator | Розділення секцій | Horizontal/vertical |
| Skeleton | Loading states | Pulse animation, responsive |
Що далі?
Тепер ви знаєте, як використовувати найпростіші компоненти. У наступній главі ми перейдемо до компонентів форм — Input, Select, Checkbox, Radio, і найголовніше — інтеграцію з React Hook Form для валідації.
Установка та Налаштування shadcn/ui
Тепер, коли ви розумієте філософію shadcn/ui, час перейти від теорії до практики. У цій главі ми встановимо shadcn/ui, налаштуємо проєкт та додамо перші компоненти.
Компоненти Форм: Побудова Інтерактивних Form
Форми — серце будь-якого веб-додатку. shadcn/ui надає потужні компоненти для створення форм з підтримкою React Hook Form та Zod валідації.