Архитектура

Tailwind CSS для стилизации

Контекст

Нужна система стилизации, совместимая с Nuxt UI и компонентным подходом.

Решение

Использовать Tailwind CSS как единственную систему стилизации.

Обоснование

КритерийTailwind
Nuxt UIНативная интеграция
Utility-firstБыстрая разработка
Design tokensКонсистентность
Bundle sizePurgeCSS
ResponsiveВстроенные breakpoints
Dark modeВстроенная поддержка

Конфигурация (Tailwind v4)

В Tailwind v4 конфигурация выполняется через CSS с директивой @theme:

/* app/assets/css/main.css */
@import "tailwindcss";

@theme {
  /* Кастомные spacing */
  --spacing-128: 32rem;

  /* Кастомные breakpoints */
  --breakpoint-3xl: 1920px;
}

Важно: Кастомизация брендовых токенов (цвета, шрифты) выполняется в UI layer. См. adr-003-shared-modules.

Правила использования

Используй utility классы

<!-- Хорошо -->
<div class="flex items-center gap-4 p-4 bg-white rounded-lg shadow">
  <span class="text-lg font-medium text-gray-900">Title</span>
</div>

Responsive дизайн (mobile-first)

<div class="
  flex flex-col        /* mobile */
  md:flex-row          /* tablet+ */
  lg:gap-8             /* desktop+ */
">

Dark mode

Nuxt UI управляет dark mode автоматически через @nuxtjs/color-mode. Стратегия — class (не media).

<!-- Утилиты dark: применяются в тёмной теме -->
<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">

<!-- Для переключения темы используй Nuxt UI -->
<UColorModeButton />

Темизация через CSS variables (Tailwind v4)

В Tailwind v4 кастомные значения определяются через CSS @theme, а не через tailwind.config.ts:

/* assets/css/main.css */
@import "tailwindcss";

@theme {
  --color-brand-50: oklch(98% 0.02 250);
  --color-brand-500: oklch(55% 0.22 260);
  --color-brand-900: oklch(25% 0.08 260);

  --font-sans: 'Inter', sans-serif;
  --radius-lg: 0.75rem;
}

Определённые значения сразу доступны как утилиты: bg-brand-500, text-brand-900, font-sans.

Динамическая смена темы

Для кастомных тем (например, branding по клиенту) используй CSS custom properties:

:root {
  --ui-primary: var(--color-green-500);
}

.theme-blue {
  --ui-primary: var(--color-blue-500);
}

Nuxt UI подхватывает --ui-primary автоматически — все компоненты обновляются без перезагрузки.

Не используй @apply

// ❌ Плохо — теряем utility-first подход
.btn {
  @apply px-4 py-2 rounded bg-blue-500 text-white;
}

// ✅ Хорошо — используй Nuxt UI компонент
<UButton>Click me</UButton>

Декомпозиция компонентов вместо @apply

Если строка классов становится слишком длинной — это сигнал к декомпозиции на компоненты.

<!-- ❌ Плохо — слишком много классов в одном элементе -->
<template>
  <div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md border border-gray-200 hover:shadow-lg transition-shadow duration-200">
    <div class="flex items-center gap-3">
      <div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
        <UIcon name="i-heroicons-user" class="w-5 h-5 text-blue-600" />
      </div>
      <div class="flex flex-col">
        <span class="text-sm font-medium text-gray-900">{{ user.name }}</span>
        <span class="text-xs text-gray-500">{{ user.email }}</span>
      </div>
    </div>
    <UButton size="xs" color="gray" variant="ghost">Edit</UButton>
  </div>
</template>
<!-- ✅ Хорошо — декомпозиция на компоненты -->
<!-- components/UserCard.vue -->
<template>
  <div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md border border-gray-200 hover:shadow-lg transition-shadow">
    <UserAvatar :user="user" />
    <UButton size="xs" color="gray" variant="ghost">Edit</UButton>
  </div>
</template>

<!-- components/UserAvatar.vue -->
<template>
  <div class="flex items-center gap-3">
    <div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
      <UIcon name="i-heroicons-user" class="w-5 h-5 text-blue-600" />
    </div>
    <div class="flex flex-col">
      <span class="text-sm font-medium text-gray-900">{{ user.name }}</span>
      <span class="text-xs text-gray-500">{{ user.email }}</span>
    </div>
  </div>
</template>

Правило: Если блок классов > 5-7 утилит — выносить в отдельный компонент.

Последствия

Положительные

  • Быстрая разработка UI
  • Консистентные spacing, colors
  • Маленький production bundle
  • Интеграция с Nuxt UI

Отрицательные

  • Learning curve для новичков
  • Длинные строки классов

Связанные решения