Складні Компоненти: Dialog, Dropdown, Table та Command
Складні Компоненти: 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: Кнопка, що відкриває dialogDialogContent: Саме вікно діалогу (рендериться в Portal)DialogHeader: Шапка з title та descriptionDialogTitle: Заголовок (для 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:
| Dialog | AlertDialog |
|---|---|
| Форми, редагування, інформація | Критичні підтвердження |
| Можна закрити кліком поза діалогом | НЕ закривається кліком поза (тільки кнопки) |
| Менш агресивний | Привертає увагу |
Dropdown Menu: Контекстні Меню
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:
| Popover | Tooltip |
|---|---|
| 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 |
| Popover | Rich content | Позиціонування, collision detection |
| Tooltip | Прості підказки | Hover, швидкий показ |
| Table | Складні дані | TanStack Table, sorting, selection |
| Accordion | FAQ, згортання | Single/multiple mode |
| Tabs | Організація контенту | Keyboard navigation |
| Command | Command Palette | Fuzzy search, shortcuts |
Що Далі?
Вітаю! 🎉 Ви пройшли повний курс по shadcn/ui:
- ✅ Вступ до UI бібліотек та їх типології
- ✅ Філософія shadcn/ui та copy-paste підхід
- ✅ Установка та налаштування
- ✅ Базові компоненти (Button, Card, Badge)
- ✅ Компоненти форм (Input, Select, Form з валідацією)
- ✅ Складні компоненти (Dialog, Table, Command)
Практичні next steps:
- Створіть реальний проєкт з shadcn/ui
- Експериментуйте з кастомізацією компонентів
- Інтегруйте з React Hook Form у складних формах
- Вивчіть решту компонентів з офіційної документації
Ресурси:
- 📚 Офіційна документація shadcn/ui
- 🎨 Radix UI — primitives
- 🌈 Tailwind CSS — styling
- 📝 React Hook Form — forms
- ✅ Zod — validation
Успіхів у створенні красивих інтерфейсів! 🚀