Урок 15. Старый vs Новый: @apply против cva
⚡ Кратко
Раньше повторы выносили в CSS-классы через @apply и получали
обратно все проблемы именования. Теперь единица переиспользования — компонент,
а варианты описывает cva.
Кнопка: два подхода
❌ @apply-классы
@layer components {
.btn { @apply inline-flex items-center rounded-md px-4 py-2 text-sm font-medium; }
.btn-primary { @apply btn bg-slate-900 text-white hover:bg-slate-700; }
.btn-outline { @apply btn border border-slate-300 text-slate-700 hover:bg-slate-50; }
.btn-sm { @apply h-8 px-3 text-xs; }
}
Проблемы: снова придумываем имена; классы остаются в CSS после удаления компонента; чтобы переопределить отступ, нужно поднимать специфичность; нет подсказок редактора о доступных вариантах.
✅ Компонент с cva
// src/components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors " +
"focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-slate-900 text-white hover:bg-slate-700",
outline: "border border-slate-300 bg-white text-slate-700 hover:bg-slate-50",
ghost: "text-slate-700 hover:bg-slate-100",
destructive: "bg-rose-600 text-white hover:bg-rose-700",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-9 px-4",
lg: "h-11 px-6 text-base",
},
},
defaultVariants: { variant: "default", size: "md" },
}
)
type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>
export function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}
Что получили: типизированные варианты, автодополнение в редакторе, возможность переопределить любой класс снаружи, удаление компонента удаляет и его стили.
Сравнение по пунктам
| Критерий | @apply | Компонент + cva |
|---|---|---|
| Именование | нужно придумывать | имя компонента и есть имя |
| Мёртвый код | копится в CSS | уходит вместе с файлом |
| Переопределение снаружи | борьба с каскадом | cn() решает конфликты |
| Подсказки редактора | нет | типы вариантов |
| Видно, что делает | нужно открыть CSS | всё в одном файле |
| Подходит для чужой разметки | да | нет |
Что изменилось в самом Tailwind
| Задача | v3 | v4 |
|---|---|---|
| Своя утилита | плагин на JavaScript | @utility |
| Свой вариант | плагин addVariant | @custom-variant |
| Слой компонентов | @layer components | остался, но применяется реже |
| Настройка | JS-конфиг | CSS-директивы |
Общая мысль: Tailwind перестал требовать JavaScript
для расширения. Всё, что раньше делали плагины, теперь описывается в CSS —
а компоненты остаются задачей вашего фреймворка.