Урок 16. Примеры: что попадает в проект

📁 Раздел: shadcn/ui ⏱️ Время изучения: ~40 мин 🎯 Сложность: Средняя

⚡ Кратко

Что именно происходит при добавлении компонента и как выглядит результат. Демонстрации ниже воспроизводят внешний вид компонентов реестра на тех же токенах темы.

1. Добавление компонента: вывод команды

npx shadcn@latest add button

✔ Checking registry.
✔ Installing dependencies.
✔ Created 1 file:
  - src/components/ui/button.tsx

Обратите внимание: «Created 1 file» — это не установка пакета, а создание файла. Дальше он ваш.

2. Как выглядят варианты Button

<div class="flex flex-wrap items-center gap-3">
  <button class="inline-flex h-8 items-center justify-center rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground transition-all hover:bg-primary/80">Default</button>
  <button class="inline-flex h-8 items-center justify-center rounded-lg border border-border bg-background px-2.5 text-sm font-medium transition-all hover:bg-muted">Outline</button>
  <button class="inline-flex h-8 items-center justify-center rounded-lg bg-secondary px-2.5 text-sm font-medium text-secondary-foreground transition-all">Secondary</button>
  <button class="inline-flex h-8 items-center justify-center rounded-lg px-2.5 text-sm font-medium transition-all hover:bg-muted">Ghost</button>
  <button class="inline-flex h-8 items-center justify-center rounded-lg bg-destructive/10 px-2.5 text-sm font-medium text-destructive transition-all hover:bg-destructive/20">Destructive</button>
  <button class="inline-flex h-8 items-center justify-center rounded-lg px-2.5 text-sm font-medium text-primary underline-offset-4 hover:underline">Link</button>
</div>

Все шесть вариантов описаны в одном файле таблицей cva — ровно так, как мы делали в уроке 15, только с семантическими токенами вместо конкретных цветов.

3. Карточка из подкомпонентов

Ежемесячный отчёт
Данные за июль 2026

Карточка собирается из Card, CardHeader, CardTitle, CardDescription, CardContent и CardFooter.

<div class="max-w-sm overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10">
  <div class="grid auto-rows-min items-start gap-1 px-4">
    <div class="text-base leading-snug font-medium">Ежемесячный отчёт</div>
    <div class="text-sm text-muted-foreground">Данные за июль 2026</div>
  </div>
  <div class="px-4">
    <p class="text-muted-foreground">Карточка собирается из Card, CardHeader, CardTitle, CardDescription, CardContent и CardFooter.</p>
  </div>
  <div class="flex items-center justify-end gap-2 rounded-b-xl border-t bg-muted/50 p-4">
    <button class="inline-flex h-8 items-center justify-center rounded-lg px-2.5 text-sm font-medium hover:bg-muted">Отмена</button>
    <button class="inline-flex h-8 items-center justify-center rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground">Открыть</button>
  </div>
</div>
<Card>
  <CardHeader>
    <CardTitle>Ежемесячный отчёт</CardTitle>
    <CardDescription>Данные за июль 2026</CardDescription>
  </CardHeader>
  <CardContent>…</CardContent>
  <CardFooter>
    <Button variant="ghost">Отмена</Button>
    <Button>Открыть</Button>
  </CardFooter>
</Card>

4. Правка компонента — обычное редактирование файла

// src/components/ui/button.tsx — добавляем свой вариант
const buttonVariants = cva("…", {
  variants: {
    variant: {
      default: "bg-primary text-primary-foreground hover:bg-primary/80",
      outline: "border-border bg-background hover:bg-muted",
      // ← ваш вариант: столько же прав, сколько у встроенных
      brand: "bg-brand text-brand-foreground hover:opacity-90",
    },
  },
})
<button class="inline-flex h-8 items-center justify-center rounded-lg bg-brand px-3 text-sm font-medium text-brand-foreground transition-all hover:opacity-90">
  variant="brand"
</button>

С обычной библиотекой такой вариант пришлось бы «доклеивать» снаружи через sx, styled() или переопределение темы.

5. Атрибуты-слоты

<div data-slot="card">
  <div data-slot="card-header">
    <div data-slot="card-title">Заголовок</div>
  </div>
</div>

Каждая часть компонента помечена data-slot. Это удобно и для стилизации (has-data-[slot=card-footer]:pb-0), и для тестов: селектор по слоту устойчивее, чем по классам.

6. Доступность, которую вы получаете бесплатно

<Dialog>
  <DialogTrigger render={<Button>Открыть</Button>} />
  <DialogContent>
    <DialogTitle>Подтвердите действие</DialogTitle>
  </DialogContent>
</Dialog>

За четырьмя строками — фокус-ловушка, возврат фокуса на кнопку после закрытия, закрытие по Esc, блокировка прокрутки, портал в конец документа, связка aria-labelledby с заголовком. Написать это вручную — несколько дней работы и десятки краевых случаев.