Урок 15. Старый vs Новый: @apply против cva

📁 Раздел: Организация кода ⏱️ Время изучения: ~55 мин 🎯 Сложность: Средняя

⚡ Кратко

Раньше повторы выносили в 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

Задачаv3v4
Своя утилитаплагин на JavaScript@utility
Свой вариантплагин addVariant@custom-variant
Слой компонентов@layer componentsостался, но применяется реже
НастройкаJS-конфигCSS-директивы
Общая мысль: Tailwind перестал требовать JavaScript для расширения. Всё, что раньше делали плагины, теперь описывается в CSS — а компоненты остаются задачей вашего фреймворка.