Tailwind CSSSeniorSystem design

Каковы лучшие практики организации Tailwind-классов в больших кодовых базах?

В больших кодовых базах: стандартизировать порядок классов через prettier-plugin-tailwindcss, организовать компонентный слой через cva/class-variance-authority, ввести дизайн-токены вместо сырых значений, настроить ESLint-плагин для запрета антипаттернов.

Лучшие практики организации Tailwind-классов в больших кодовых базах

При 50+ компонентах и команде из нескольких разработчиков Tailwind требует архитектурных решений, чтобы кодовая база оставалась поддерживаемой.

1. Автоматическая сортировка классов

Самое высокоприоритетное правило: единый порядок классов в 100% файлов через prettier-plugin-tailwindcss:

// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.js"
}

Плагин сортирует по официальному порядку Tailwind: positioning → display → spacing → sizing → typography → backgrounds → borders → effects → transitions → responsive.

2. Компонентный слой с class-variance-authority

// src/components/ui/badge.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';

const badgeVariants = cva(
  'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
  {
    variants: {
      variant: {
        default:     'border-transparent bg-primary text-primary-foreground',
        secondary:   'border-transparent bg-secondary text-secondary-foreground',
        destructive: 'border-transparent bg-destructive text-destructive-foreground',
        outline:     'text-foreground',
      },
    },
    defaultVariants: { variant: 'default' },
  }
);

interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

export function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div
      className={twMerge(badgeVariants({ variant }), className)}
      {...props}
    />
  );
}

3. Утилита cn() как стандарт слияния классов

// src/lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}
// Использование — единый паттерн по всей кодовой базе
<div
  className={cn(
    'base-styles',
    isActive && 'active-styles',
    hasError && 'error-styles',
    className
  )}
/>

4. Структура директорий для UI-компонентов

src/
  components/
    ui/          // Примитивы: Button, Input, Badge, Card
    patterns/    // Составные: SearchBar, FilterPanel
    layouts/     // Layout-компоненты: PageLayout, Sidebar
  styles/
    globals.css  // @theme, @layer base
    tokens.css   // Дизайн-токены

5. ESLint для запрета антипаттернов

// eslint.config.js
import tailwind from 'eslint-plugin-tailwindcss';

export default [
  ...tailwind.configs['flat/recommended'],
  {
    rules: {
      'tailwindcss/no-custom-classname': 'error', // без произвольных классов без @theme
      'tailwindcss/no-contradicting-classname': 'error', // p-4 + p-8 одновременно
      'tailwindcss/enforces-shorthand': 'warn',   // px-4 py-4 → p-4
    },
  },
];

6. Ограничение использования @apply

@apply допустим только в globals.css для глобальных базовых стилей и в CSS Modules. В JSX/TSX-файлах — запрещён. Это предотвращает скрытые зависимости и замедление сборки.

7. Документация компонентов в Storybook

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  tags: ['autodocs'],
};
export default meta;

export const AllVariants: StoryObj = {
  render: () => (
    <div className="flex flex-wrap gap-4">
      <Button intent="primary">Primary</Button>
      <Button intent="secondary">Secondary</Button>
      <Button intent="destructive">Destructive</Button>
    </div>
  ),
};

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

  • Отсутствие prettier-plugin-tailwindcss с первого дня: ретроактивное применение генерирует огромный diff без логических изменений.
  • Компоненты без className-пропса: нельзя кастомизировать на месте использования.
  • Дублирование вариантов вручную вместо cva: изменение дефолтного стиля требует grep по всему репозиторию.
  • Смешение twMerge и простой конкатенации строк в одной кодовой базе — конфликты классов в непредсказуемых местах.
  • Inline style={{}} рядом с Tailwind-утилитами для «сложных» значений — нарушение единственного источника истины.
  • Отсутствие lint-правил: разработчики добавляют произвольные значения (w-[347px]) вместо стандартных токенов.
  • Компонентная библиотека без экспорта типов вариантов (VariantProps) — потребители не видят доступные пропсы.
  • Storybook без PostCSS-конфигурации Tailwind не рендерит стили компонентов.

Common mistakes

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

What the interviewer is testing

  • Объясняет стандарты сортировки, токены, компоненты и ревью правил в больших командах.
  • Показывает на примере, как работает: лучшие практики включают Prettier-сортировку, компонентные variant APIs, ограничение arbitrary values, общий token layer и документацию паттернов для повторяемых интерфейсных решений.
  • Называет production-нюанс и граничный случай для темы «организация Tailwind-классов».

Sources

Related topics