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

Базові Компоненти shadcn/ui: Фундамент Інтерфейсу

Тепер, коли проєкт налаштований, час розібрати базові компоненти 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, вага 500
  • transition-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>
    )
}

Як це працює:

  1. Radix намагається завантажити зображення
  2. Поки завантажується → показує AvatarFallback (CN)
  3. Якщо завантажилось → показує AvatarImage
  4. Якщо помилка завантаження → залишається 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
SkeletonLoading statesPulse animation, responsive

Що далі?

Тепер ви знаєте, як використовувати найпростіші компоненти. У наступній главі ми перейдемо до компонентів форм — Input, Select, Checkbox, Radio, і найголовніше — інтеграцію з React Hook Form для валідації.

Далі: Компоненти Форм →

Copyright © 2026