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

Складні Компоненти: Dialog, Dropdown, Table та Command

Тепер, коли ми освоїли базові компоненти та форми, час перейти до складніших UI patterns. У цій главі ми розберемо компоненти, які роблять інтерфейс по-справжньому інтерактивним та зручним.

Складні Компоненти: Dialog, Dropdown, Table та Command

Тепер, коли ми освоїли базові компоненти та форми, час перейти до складніших UI patterns. У цій главі ми розберемо компоненти, які роблять інтерфейс по-справжньому інтерактивним та зручним.

Що ми вивчимо:

  • Dialog та Alert Dialog — модальні вікна
  • Dropdown Menu та Context Menu — контекстні меню
  • Popover та Tooltip — додаткова інформація
  • Table — складні таблиці з TanStack Table
  • Accordion та Tabs — організація контенту
  • Command Palette — швидкий доступ до функцій

Dialog: Модальні Вікна

Dialog — компонент для модальних вікон, що блокують взаємодію з основним контентом.

Додавання

npx shadcn@latest add dialog

Анатомія Складної Композиції

import {
    Dialog,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
    DialogClose,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

export function DialogDemo() {
    return (
        <Dialog>
            <DialogTrigger asChild>
                <Button variant="outline">Edit Profile</Button>
            </DialogTrigger>
            <DialogContent className="sm:max-w-[425px]">
                <DialogHeader>
                    <DialogTitle>Edit profile</DialogTitle>
                    <DialogDescription>
                        Make changes to your profile here. Click save when you're done.
                    </DialogDescription>
                </DialogHeader>
                <div className="grid gap-4 py-4">
                    <div className="grid grid-cols-4 items-center gap-4">
                        <Label htmlFor="name" className="text-right">
                            Name
                        </Label>
                        <Input id="name" defaultValue="Pedro Duarte" className="col-span-3" />
                    </div>
                    <div className="grid grid-cols-4 items-center gap-4">
                        <Label htmlFor="username" className="text-right">
                            Username
                        </Label>
                        <Input id="username" defaultValue="@peduarte" className="col-span-3" />
                    </div>
                </div>
                <DialogFooter>
                    <DialogClose asChild>
                        <Button variant="secondary">Cancel</Button>
                    </DialogClose>
                    <Button type="submit">Save changes</Button>
                </DialogFooter>
            </DialogContent>
        </Dialog>
    )
}

Розбір компонентів:

  • Dialog: Root component (керує станом open/closed)
  • DialogTrigger: Кнопка, що відкриває dialog
  • DialogContent: Саме вікно діалогу (рендериться в Portal)
  • DialogHeader: Шапка з title та description
  • DialogTitle: Заголовок (для accessibility)
  • DialogDescription: Опис (для screen readers)
  • DialogFooter: Футер з кнопками (Cancel, Save)
  • DialogClose: Закриває dialog

Що Radix робить під капотом:

  • ✅ Focus trap (не можна вийти Tab-ом)
  • ✅ Escape закриває
  • ✅ Клік поза діалогом закриває
  • ✅ Блокує scroll на body
  • ✅ Повертає фокус до trigger після закриття
  • ✅ ARIA attributes (role="dialog", aria-labelledby, aria-describedby)

Controlled Dialog

function ControlledDialog() {
    const [open, setOpen] = useState(false)

    return (
        <Dialog open={open} onOpenChange={setOpen}>
            {/* ... */}
        </Dialog>
    )
}

Alert Dialog: Критичні Дії

Alert Dialog — для підтвердження важливих дій (видалення, відміна).

npx shadcn@latest add alert-dialog
import {
    AlertDialog,
    AlertDialogAction,
    AlertDialogCancel,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
    AlertDialogTrigger,
} from '@/components/ui/alert-dialog'

export function AlertDialogDemo() {
    return (
        <AlertDialog>
            <AlertDialogTrigger asChild>
                <Button variant="destructive">Delete Account</Button>
            </AlertDialogTrigger>
            <AlertDialogContent>
                <AlertDialogHeader>
                    <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
                    <AlertDialogDescription>
                        This action cannot be undone. This will permanently delete your account and remove your data
                        from our servers.
                    </AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                    <AlertDialogCancel>Cancel</AlertDialogCancel>
                    <AlertDialogAction>Continue</AlertDialogAction>
                </AlertDialogFooter>
            </AlertDialogContent>
        </AlertDialog>
    )
}

Розмір AlertDialogContent:

AlertDialogContent підтримує prop size для контролю розміру:

{
    /* Стандартний розмір (за замовчуванням) */
}
;<AlertDialogContent>{/* content */}</AlertDialogContent>

{
    /* Компактний розмір */
}
;<AlertDialogContent size="sm">{/* content */}</AlertDialogContent>

Різниця між Dialog та AlertDialog:

DialogAlertDialog
Форми, редагування, інформаціяКритичні підтвердження
Можна закрити кліком поза діалогомНЕ закривається кліком поза (тільки кнопки)
Менш агресивнийПривертає увагу

Dropdown Menu — для дій, налаштувань, навігації.

Додавання

npx shadcn@latest add dropdown-menu

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

import {
    DropdownMenu,
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenuLabel,
    DropdownMenuSeparator,
    DropdownMenuTrigger,
    DropdownMenuGroup,
    DropdownMenuShortcut,
    DropdownMenuCheckboxItem,
    DropdownMenuRadioGroup,
    DropdownMenuRadioItem,
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'

export function DropdownMenuDemo() {
    return (
        <DropdownMenu>
            <DropdownMenuTrigger asChild>
                <Button variant="outline">Open Menu</Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent className="w-56">
                <DropdownMenuLabel>My Account</DropdownMenuLabel>
                <DropdownMenuSeparator />
                <DropdownMenuGroup>
                    <DropdownMenuItem>
                        Profile
                        <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
                    </DropdownMenuItem>
                    <DropdownMenuItem>
                        Settings
                        <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
                    </DropdownMenuItem>
                </DropdownMenuGroup>
                <DropdownMenuSeparator />
                <DropdownMenuItem>
                    Log out
                    <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
                </DropdownMenuItem>
            </DropdownMenuContent>
        </DropdownMenu>
    )
}

Складніші Features

Checkbox Items (Multiple Selection):

const [showStatusBar, setShowStatusBar] = useState(true)
const [showActivityBar, setShowActivityBar] = useState(false)

<DropdownMenuCheckboxItem
  checked={showStatusBar}
  onCheckedChange={setShowStatusBar}
>
  Status Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
  checked={showActivityBar}
  onCheckedChange={setShowActivityBar}
>
  Activity Bar
</DropdownMenuCheckboxItem>

RadioGroup Items (Single Selection):

const [position, setPosition] = useState("bottom")

<DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
  <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
  <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
  <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>

Context Menu

Context Menu — правий клік меню.

npx shadcn@latest add context-menu
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'
;<ContextMenu>
    <ContextMenuTrigger className="border rounded-md p-12">Right click here</ContextMenuTrigger>
    <ContextMenuContent>
        <ContextMenuItem>Copy</ContextMenuItem>
        <ContextMenuItem>Paste</ContextMenuItem>
        <ContextMenuItem>Delete</ContextMenuItem>
    </ContextMenuContent>
</ContextMenu>


Popover та Tooltip

Popover

Popover — для rich content (форми, додаткова інформація).

npx shadcn@latest add popover
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
;<Popover>
    <PopoverTrigger asChild>
        <Button variant="outline">Open popover</Button>
    </PopoverTrigger>
    <PopoverContent className="w-80">
        <div className="grid gap-4">
            <div className="space-y-2">
                <h4 className="font-medium leading-none">Dimensions</h4>
                <p className="text-sm text-muted-foreground">Set the dimensions for the layer.</p>
            </div>
            <div className="grid gap-2">
                <Label htmlFor="width">Width</Label>
                <Input id="width" defaultValue="100%" />
            </div>
        </div>
    </PopoverContent>
</Popover>

Tooltip

Tooltip — для простих підказок.

npx shadcn@latest add tooltip
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
;<TooltipProvider>
    <Tooltip>
        <TooltipTrigger asChild>
            <Button variant="outline">Hover</Button>
        </TooltipTrigger>
        <TooltipContent>
            <p>Add to library</p>
        </TooltipContent>
    </Tooltip>
</TooltipProvider>

Popover vs Tooltip:

PopoverTooltip
Rich content (forms, images)Simple text
Клік для відкриттяHover для показу
Тривалий показМить

Table: Складні Дані з TanStack Table

shadcn/ui Table інтегрується з TanStack Table для sorting, filtering, pagination.

Додавання

npx shadcn@latest add table
npm install @tanstack/react-table

Базова Таблиця

import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'

const invoices = [
    { invoice: 'INV001', status: 'Paid', amount: '$250.00' },
    { invoice: 'INV002', status: 'Pending', amount: '$150.00' },
    // ...
]

export function TableDemo() {
    return (
        <Table>
            <TableCaption>A list of your recent invoices.</TableCaption>
            <TableHeader>
                <TableRow>
                    <TableHead>Invoice</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead className="text-right">Amount</TableHead>
                </TableRow>
            </TableHeader>
            <TableBody>
                {invoices.map((invoice) => (
                    <TableRow key={invoice.invoice}>
                        <TableCell className="font-medium">{invoice.invoice}</TableCell>
                        <TableCell>{invoice.status}</TableCell>
                        <TableCell className="text-right">{invoice.amount}</TableCell>
                    </TableRow>
                ))}
            </TableBody>
        </Table>
    )
}

Інтеграція з TanStack Table

Повний приклад з sorting та selection:

'use client'

import * as React from 'react'
import {
    ColumnDef,
    flexRender,
    getCoreRowModel,
    getSortedRowModel,
    SortingState,
    useReactTable,
} from '@tanstack/react-table'
import { ArrowUpDown } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'

type Payment = {
    id: string
    amount: number
    status: 'pending' | 'processing' | 'success' | 'failed'
    email: string
}

const data: Payment[] = [
    {
        id: 'm5gr84i9',
        amount: 316,
        status: 'success',
        email: 'ken99@yahoo.com',
    },
    // ... more data
]

export function DataTableDemo() {
    const [sorting, setSorting] = React.useState<SortingState>([])
    const [rowSelection, setRowSelection] = React.useState({})

    const columns: ColumnDef<Payment>[] = [
        {
            id: 'select',
            header: ({ table }) => (
                <Checkbox
                    checked={table.getIsAllPageRowsSelected()}
                    onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
                    aria-label="Select all"
                />
            ),
            cell: ({ row }) => (
                <Checkbox
                    checked={row.getIsSelected()}
                    onCheckedChange={(value) => row.toggleSelected(!!value)}
                    aria-label="Select row"
                />
            ),
        },
        {
            accessorKey: 'email',
            header: ({ column }) => {
                return (
                    <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
                        Email
                        <ArrowUpDown className="ml-2 h-4 w-4" />
                    </Button>
                )
            },
        },
        {
            accessorKey: 'amount',
            header: () => <div className="text-right">Amount</div>,
            cell: ({ row }) => {
                const amount = parseFloat(row.getValue('amount'))
                const formatted = new Intl.NumberFormat('en-US', {
                    style: 'currency',
                    currency: 'USD',
                }).format(amount)

                return <div className="text-right font-medium">{formatted}</div>
            },
        },
        {
            accessorKey: 'status',
            header: 'Status',
        },
    ]

    const table = useReactTable({
        data,
        columns,
        getCoreRowModel: getCoreRowModel(),
        getSortedRowModel: getSortedRowModel(),
        onSortingChange: setSorting,
        onRowSelectionChange: setRowSelection,
        state: {
            sorting,
            rowSelection,
        },
    })

    return (
        <div className="rounded-md border">
            <Table>
                <TableHeader>
                    {table.getHeaderGroups().map((headerGroup) => (
                        <TableRow key={headerGroup.id}>
                            {headerGroup.headers.map((header) => {
                                return (
                                    <TableHead key={header.id}>
                                        {header.isPlaceholder
                                            ? null
                                            : flexRender(header.column.columnDef.header, header.getContext())}
                                    </TableHead>
                                )
                            })}
                        </TableRow>
                    ))}
                </TableHeader>
                <TableBody>
                    {table.getRowModel().rows?.length ? (
                        table.getRowModel().rows.map((row) => (
                            <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
                                {row.getVisibleCells().map((cell) => (
                                    <TableCell key={cell.id}>
                                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
                                    </TableCell>
                                ))}
                            </TableRow>
                        ))
                    ) : (
                        <TableRow>
                            <TableCell colSpan={columns.length} className="h-24 text-center">
                                No results.
                            </TableCell>
                        </TableRow>
                    )}
                </TableBody>
            </Table>
        </div>
    )
}

Features:

  • ✅ Сортування (клік на header)
  • ✅ Row selection (checkboxes)
  • ✅ Кастомні cell рендери (форматування amount)


Accordion та Tabs

Accordion

Accordion — для FAQ, згортаємих секцій.

npx shadcn@latest add accordion
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
;<Accordion type="single" collapsible className="w-full">
    <AccordionItem value="item-1">
        <AccordionTrigger>Is it accessible?</AccordionTrigger>
        <AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
    </AccordionItem>
    <AccordionItem value="item-2">
        <AccordionTrigger>Is it styled?</AccordionTrigger>
        <AccordionContent>Yes. It comes with default styles that match the other components.</AccordionContent>
    </AccordionItem>
</Accordion>

Modes:

  • type="single": Тільки один відкритий
  • type="multiple": Кілька відкритих одночасно

Tabs

npx shadcn@latest add tabs
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
;<Tabs defaultValue="account" className="w-[400px]">
    <TabsList className="grid w-full grid-cols-2">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
    </TabsList>
    <TabsContent value="account">
        <Card>
            <CardHeader>
                <CardTitle>Account</CardTitle>
            </CardHeader>
            <CardContent className="space-y-2">{/* Account form */}</CardContent>
        </Card>
    </TabsContent>
    <TabsContent value="password">
        <Card>
            <CardHeader>
                <CardTitle>Password</CardTitle>
            </CardHeader>
            <CardContent className="space-y-2">{/* Password form */}</CardContent>
        </Card>
    </TabsContent>
</Tabs>

Command Palette: Швидкий Доступ

Command Palette — потужний компонент для швидкого доступу (Cmd+K).

Додавання

npx shadcn@latest add command dialog

Повноцінний Command Palette

'use client'

import * as React from 'react'
import { useRouter } from 'next/navigation'
import { Calculator, Calendar, Settings, Smile, User } from 'lucide-react'

import {
    CommandDialog,
    CommandEmpty,
    CommandGroup,
    CommandInput,
    CommandItem,
    CommandList,
    CommandSeparator,
    CommandShortcut,
} from '@/components/ui/command'

export function CommandMenu() {
    const router = useRouter()
    const [open, setOpen] = React.useState(false)

    React.useEffect(() => {
        const down = (e: KeyboardEvent) => {
            if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
                e.preventDefault()
                setOpen((open) => !open)
            }
        }

        document.addEventListener('keydown', down)
        return () => document.removeEventListener('keydown', down)
    }, [])

    const runCommand = React.useCallback((command: () => unknown) => {
        setOpen(false)
        command()
    }, [])

    return (
        <CommandDialog open={open} onOpenChange={setOpen}>
            <CommandInput placeholder="Type a command or search..." />
            <CommandList>
                <CommandEmpty>No results found.</CommandEmpty>
                <CommandGroup heading="Suggestions">
                    <CommandItem onSelect={() => runCommand(() => router.push('/calendar'))}>
                        <Calendar className="mr-2 h-4 w-4" />
                        <span>Calendar</span>
                    </CommandItem>
                    <CommandItem onSelect={() => runCommand(() => router.push('/settings'))}>
                        <Settings className="mr-2 h-4 w-4" />
                        <span>Settings</span>
                        <CommandShortcut>⌘S</CommandShortcut>
                    </CommandItem>
                </CommandGroup>
                <CommandSeparator />
                <CommandGroup heading="Settings">
                    <CommandItem>
                        <User className="mr-2 h-4 w-4" />
                        <span>Profile</span>
                        <CommandShortcut>⌘P</CommandShortcut>
                    </CommandItem>
                </CommandGroup>
            </CommandList>
        </CommandDialog>
    )
}

Features:

  • ⌨️ Cmd+K shortcut
  • 🔍 Fuzzy search
  • 🏷️ Групування команд
  • ➡️ Keyboard navigation


Підсумок

Ми освоїли складні компоненти shadcn/ui:

КомпонентВикористанняКлючові Features
DialogМодальні вікнаFocus trap, Escape close, Portal
Alert DialogКритичні підтвердженняНе закривається кліком поза
Dropdown MenuДії, налаштуванняKeyboard navigation, shortcuts
Context MenuПравий клікТе саме, що Dropdown
PopoverRich contentПозиціонування, collision detection
TooltipПрості підказкиHover, швидкий показ
TableСкладні даніTanStack Table, sorting, selection
AccordionFAQ, згортанняSingle/multiple mode
TabsОрганізація контентуKeyboard navigation
CommandCommand PaletteFuzzy search, shortcuts

Що Далі?

Вітаю! 🎉 Ви пройшли повний курс по shadcn/ui:

  1. ✅ Вступ до UI бібліотек та їх типології
  2. ✅ Філософія shadcn/ui та copy-paste підхід
  3. ✅ Установка та налаштування
  4. ✅ Базові компоненти (Button, Card, Badge)
  5. ✅ Компоненти форм (Input, Select, Form з валідацією)
  6. ✅ Складні компоненти (Dialog, Table, Command)

Практичні next steps:

  1. Створіть реальний проєкт з shadcn/ui
  2. Експериментуйте з кастомізацією компонентів
  3. Інтегруйте з React Hook Form у складних формах
  4. Вивчіть решту компонентів з офіційної документації

Ресурси:

Успіхів у створенні красивих інтерфейсів! 🚀

Copyright © 2026