Урок 18. Примеры: расширяем компонент
⚡ Кратко
Расширяем компонент: свой вариант, свой размер, кнопка-ссылка, кнопка с иконкой и состояние загрузки.
1. Добавляем фирменный вариант
// src/components/ui/button.tsx
const buttonVariants = cva("…базовые классы…", {
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
// ← новый вариант: работает наравне со встроенными
brand: "bg-brand text-brand-foreground hover:opacity-90",
},
},
})
<Button variant="brand">Оформить подписку</Button>
<button class="inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-lg bg-brand px-2.5 text-sm font-medium whitespace-nowrap text-brand-foreground transition-all hover:opacity-90">
Оформить подписку
</button>
TypeScript немедленно подхватит новое значение: в подсказках
variant появится "brand".
2. Добавляем размер
size: {
default: "h-8 gap-1.5 px-2.5",
lg: "h-9 gap-1.5 px-2.5",
xl: "h-11 gap-2 px-5 text-base [&_svg:not([class*='size-'])]:size-5",
}
<div class="flex flex-wrap items-center gap-3">
<button class="inline-flex h-8 items-center justify-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground">default</button>
<button class="inline-flex h-9 items-center justify-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground">lg</button>
<button class="inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-primary px-5 text-base font-medium text-primary-foreground">xl</button>
</div>
Заметьте: вместе с высотой меняется и размер иконки внутри — тот самый
приём с [&_svg:not([class*='size-'])].
3. Кнопка-ссылка
<Button render={<a href="/reports" />}>Открыть отчёты</Button>
<a href="#" class="inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium whitespace-nowrap text-primary-foreground transition-all hover:bg-primary/80">
Открыть отчёты
</a>
В разметке это настоящий <a>: работает средний клик,
«открыть в новой вкладке» и переход по Enter. Кнопка, стилизованная
под ссылку, всего этого не умеет.
4. Иконка и подпись
import { PlusIcon, DownloadIcon } from "lucide-react"
<Button><PlusIcon /> Добавить</Button>
<Button variant="outline">Скачать <DownloadIcon data-icon="inline-end" /></Button>
<Button size="icon" aria-label="Настройки"><SettingsIcon /></Button>
<div class="flex flex-wrap items-center gap-3">
<button class="inline-flex h-8 items-center justify-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground">
<svg class="size-4 shrink-0" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
Добавить
</button>
<button class="inline-flex h-8 items-center justify-center gap-1.5 rounded-lg border border-border bg-background px-2.5 text-sm font-medium hover:bg-muted">
Скачать
<svg class="size-4 shrink-0" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M10 4v9m0 0l-3-3m3 3l3-3M4 16h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<button class="inline-flex size-8 items-center justify-center rounded-lg border border-border bg-background hover:bg-muted" aria-label="Настройки">
<svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="10" cy="10" r="3"/><path d="M10 2v2m0 12v2M2 10h2m12 0h2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
У кнопки-иконки обязателен aria-label: без текста
скринридер не сможет её назвать.
5. Состояние загрузки
function SaveButton({ isLoading }: { isLoading: boolean }) {
return (
<Button disabled={isLoading}>
{isLoading && <LoaderIcon className="animate-spin" />}
{isLoading ? "Сохраняем…" : "Сохранить"}
</Button>
)
}
<div class="flex flex-wrap items-center gap-3">
<button class="inline-flex h-8 items-center justify-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground">Сохранить</button>
<button class="inline-flex h-8 items-center justify-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground opacity-50" disabled>
<span class="size-4 animate-spin rounded-full border-2 border-primary-foreground/40 border-t-primary-foreground"></span>
Сохраняем…
</button>
</div>
Класс disabled:opacity-50 уже есть в базовых классах —
компоненту достаточно передать проп disabled.
6. Использование buttonVariants без компонента
Файл экспортирует не только Button, но и
buttonVariants. Это позволяет оформить как кнопку что угодно:
import { buttonVariants } from "@/components/ui/button"
<Link to="/settings" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
Настройки
</Link>
Приём полезен для компонентов маршрутизации, которые нельзя обернуть
через render.