Tailwind CSSMiddleCoding

Как использовать Tailwind CSS с компонентным фреймворком, например React или Vue?

Tailwind отлично работает с React/Vue: классы добавляются через className/class атрибуты. Для переиспользования стилей используйте компоненты фреймворка, @apply в CSS, или утилиту clsx/cva для условных классов.

Tailwind CSS с React и Vue

Tailwind идеально сочетается с компонентными фреймворками: стили инкапсулируются в компонентах вместо CSS-файлов, а JIT-режим отслеживает классы в JSX/Vue-шаблонах.

Установка в Vite-проект (React)

// 1. Установка (через контейнер или проектный пакетный менеджер)
// npm install -D tailwindcss @tailwindcss/vite

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite'; // v4

export default defineConfig({
  plugins: [react(), tailwindcss()],
});
/* src/index.css */
@import "tailwindcss";

Базовое использование в React

// Button.tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary';
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
}

export function Button({ variant = 'primary', children, onClick, disabled }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`
        px-4 py-2 rounded-lg font-semibold transition-colors
        focus:outline-none focus:ring-2 focus:ring-offset-2
        disabled:opacity-50 disabled:cursor-not-allowed
        ${variant === 'primary'
          ? 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500'
          : 'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-gray-400'
        }
      `}
    >
      {children}
    </button>
  );
}

clsx + tailwind-merge для условных классов

// npm install clsx tailwind-merge
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

// Вспомогательная функция (cn)
function cn(...inputs: (string | undefined | null | false)[]) {
  return twMerge(clsx(inputs));
}

// Использование
function Card({ className, highlighted }: { className?: string; highlighted?: boolean }) {
  return (
    <div
      className={cn(
        'rounded-xl border bg-white p-6 shadow-sm',
        highlighted && 'border-blue-500 shadow-blue-100',
        className // позволяет переопределить снаружи
      )}
    />
  );
}

cva (Class Variance Authority) для вариантов компонентов

// npm install class-variance-authority
import { cva, type VariantProps } from 'class-variance-authority';

const button = cva(
  // Базовые классы
  'inline-flex items-center justify-center rounded-lg font-semibold transition-colors focus:outline-none',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
        danger: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4',
        lg: 'h-12 px-6 text-lg',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
  & VariantProps<typeof button>;

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return <button className={button({ variant, size, className })} {...props} />;
}

// Использование
<Button variant="danger" size="lg">Удалить</Button>

Vue 3 SFC

// Card.vue
<template>
  <div
    :class="[
      'rounded-xl border p-6 transition-shadow',
      highlighted ? 'border-blue-500 shadow-lg' : 'border-gray-200 shadow-sm'
    ]"
  >
    <slot />
  </div>
</template>

<script setup lang="ts">
defineProps<{ highlighted?: boolean }>();
</script>

@apply для переиспользуемых паттернов

/* styles/components.css */
@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-blue-600 text-white rounded-lg
           hover:bg-blue-700 transition-colors
           focus:outline-none focus:ring-2 focus:ring-blue-500;
  }
}

Подводные камни

  • Динамически составленные имена классов через интерполяцию строк (`bg-${color}-500`) не попадают в JIT-сборку — всегда используйте полные имена классов.
  • Без tailwind-merge переопределение классов работает непредсказуемо: p-4 p-2 — выиграет тот, кто стоит позже в CSS, а не в строке className.
  • Vue: динамические классы через :class работают, но Tailwind IntelliSense не подсвечивает их — убедитесь, что полные имена присутствуют в коде.
  • Длинные строки className в JSX ухудшают читаемость — используйте prettier-plugin-tailwindcss для автоматической сортировки классов.
  • При использовании CSS Modules вместе с Tailwind возникает конфликт скоупов — решайте через явный импорт в @layer.
  • Server Components в Next.js не поддерживают контекст и хуки — не передавайте className через контекст для серверных компонентов.
  • Избыток @apply воспроизводит проблемы обычного CSS (специфичность, порядок) — применяйте только для действительно часто используемых паттернов.

Common mistakes

  • Смешивать «Tailwind с React или Vue» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «Tailwind с React или Vue» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет композиция utilities с компонентами и пропсами без динамических class fragments.
  • Показывает на примере, как работает: в компонентных фреймворках нужно маппить props на полные строки классов, чтобы Tailwind смог обнаружить все варианты на build time.
  • Называет production-нюанс и граничный случай для темы «Tailwind с React или Vue».

Sources

Related topics