Tailwind-only стилизация
Стандарт определяет правила стилизации компонентов с использованием Tailwind CSS.
Правило: Только Tailwind
Запрещено: Использование <style> блоков в Vue-компонентах.
Разрешено: Только Tailwind-классы в атрибуте class.
<!-- ✅ Хорошо -->
<template>
<div class="flex items-center gap-4 p-4 bg-default rounded-lg">
<span class="text-lg font-medium text-default">Заголовок</span>
</div>
</template>
<script setup lang="ts">
// логика
</script>
<!-- ❌ Плохо -->
<template>
<div class="container">
<span class="title">Заголовок</span>
</div>
</template>
<script setup lang="ts">
// логика
</script>
<style scoped>
.container {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--color-bg);
border-radius: 0.5rem;
}
.title {
font-size: 1.125rem;
font-weight: 500;
}
</style>
Обоснование
Проблема со <style> | Решение с Tailwind |
|---|---|
| Разрозненные стили | Единый дизайн-токены |
| Конфликты имён классов | Атомарные классы |
| Сложность рефакторинга | Изменения видны сразу |
| Дублирование CSS | Переиспользование классов |
| Увеличение бандла | Минимальный CSS |
Исключения
Глобальные стили
Глобальные стили допустимы в app/assets/css/:
app/assets/css/
├── main.css # точка входа
├── base.css # базовые reset-стили
└── fonts.css # @font-face
Сторонние библиотеки
Переопределение стилей сторонних библиотек (редактор, календарь и т.д.):
/* app/assets/css/vendor/quill.css */
.ql-editor {
min-height: 200px;
}
CSS-переменные темы
CSS-переменные определяются в UI layer, не в компонентах.
Длинные списки классов
При большом количестве классов используй перенос и группировку:
<template>
<!-- По категориям: layout → spacing → sizing → typography → colors → effects -->
<div
class="
flex items-center justify-between
p-4 gap-4
w-full max-w-md
text-sm font-medium
bg-default text-default border-default
rounded-lg shadow-sm
transition-colors hover:bg-muted
"
>
Контент
</div>
</template>
Порядок классов
- Layout:
flex,grid,block,items-*,justify-* - Spacing:
p-*,m-*,gap-* - Sizing:
w-*,h-*,max-*,min-* - Typography:
text-*,font-*,leading-* - Colors:
bg-*,text-*,border-* - Effects:
rounded-*,shadow-*,opacity-* - States:
hover:*,focus:*,active:* - Responsive:
sm:*,md:*,lg:*
Динамические классы
Запрещено: Интерполяция в именах классов
Tailwind анализирует код статически при сборке. Динамически собранные имена классов не будут включены в CSS.
<!-- ❌ ЗАПРЕЩЕНО: класс не попадёт в бандл -->
<template>
<div :class="`bg-${color}-500`">
<div :class="`text-${size}`">
<div :class="`p-${spacing}`">
</template>
<script setup>
const color = ref('red') // bg-red-500 не будет в CSS!
const size = ref('lg') // text-lg не будет в CSS!
const spacing = ref('4') // p-4 не будет в CSS!
</script>
<!-- ✅ ПРАВИЛЬНО: полные имена классов -->
<template>
<div :class="colorClass">
<div :class="sizeClass">
</template>
<script setup>
const colorClass = computed(() => {
const colors = {
red: 'bg-red-500',
blue: 'bg-blue-500',
green: 'bg-green-500',
}
return colors[color.value]
})
const sizeClass = computed(() => {
const sizes = {
sm: 'text-sm p-2',
md: 'text-base p-4',
lg: 'text-lg p-6',
}
return sizes[size.value]
})
</script>
Условные классы
Для условных классов используй объект или массив:
<template>
<!-- Простое условие -->
<div :class="{ 'opacity-50': isDisabled }">
<!-- Несколько условий -->
<div
:class="[
'base-class',
isActive && 'bg-primary text-white',
isDisabled && 'opacity-50 pointer-events-none',
]"
>
<!-- Computed для сложной логики -->
<div :class="statusClasses">
</template>
<script setup lang="ts">
const statusClasses = computed(() => {
const base = 'px-2 py-1 rounded text-sm font-medium'
switch (status.value) {
case 'success':
return `${base} bg-success/10 text-success`
case 'error':
return `${base} bg-error/10 text-error`
default:
return `${base} bg-muted text-muted`
}
})
</script>
Tailwind-паттерны: group, peer, has
Tailwind позволяет стилизовать элементы на основе состояния родителя или соседа — то, что невозможно в обычном CSS без JavaScript.
group — стили на основе родителя
Стилизация дочерних элементов при hover/focus родителя:
<template>
<!-- Родитель с class="group" -->
<a href="/order/123" class="group block p-4 rounded-lg hover:bg-muted">
<h3 class="font-medium group-hover:text-primary">Заказ #123</h3>
<p class="text-muted group-hover:text-default">Подробнее →</p>
<UIcon
name="i-lucide-arrow-right"
class="opacity-0 group-hover:opacity-100 transition-opacity"
/>
</a>
</template>
Именованные группы
Для вложенных групп используй именование:
<template>
<div class="group/card p-4 hover:bg-muted">
<div class="group/header flex items-center">
<h3 class="group-hover/header:underline">Заголовок</h3>
</div>
<p class="group-hover/card:text-primary">Контент карточки</p>
</div>
</template>
peer — стили на основе соседа
Стилизация элемента на основе состояния предшествующего соседа:
<template>
<div>
<!-- Input с class="peer" -->
<input type="email" class="peer" placeholder="Email" />
<!-- Сообщение появляется при невалидном input -->
<p class="invisible peer-invalid:visible text-error text-sm">
Введите корректный email
</p>
<!-- Label поднимается при focus -->
<label class="peer-focus:-translate-y-6 peer-focus:text-primary transition-all">
Email
</label>
</div>
</template>
has — стили на основе потомка
Стилизация родителя на основе состояния дочернего элемента:
<template>
<!-- Карточка подсвечивается если внутри checked checkbox -->
<label class="block p-4 border rounded-lg has-[:checked]:border-primary has-[:checked]:bg-primary/5">
<input type="checkbox" class="mr-2" />
Выбрать этот вариант
</label>
<!-- Форма показывает рамку если внутри есть focus -->
<form class="p-4 rounded-lg has-[:focus]:ring-2 has-[:focus]:ring-primary">
<input type="text" placeholder="Имя" />
<input type="email" placeholder="Email" />
</form>
</template>
Таблица состояний
| Модификатор | Триггер | Пример |
|---|---|---|
group-hover: | Hover на родителе с group | group-hover:text-primary |
group-focus: | Focus на родителе | group-focus:ring-2 |
peer-hover: | Hover на предыдущем элементе с peer | peer-hover:visible |
peer-invalid: | Невалидный input-сосед | peer-invalid:text-error |
peer-checked: | Checked checkbox-сосед | peer-checked:bg-primary |
has-[:checked]: | Содержит checked элемент | has-[:checked]:border-primary |
has-[:focus]: | Содержит focused элемент | has-[:focus]:ring-2 |
Переиспользование стилей
Вариант 1: Компонент
Для переиспользуемых элементов создавай компонент:
<!-- components/StatusBadge.vue -->
<template>
<span :class="classes">
<slot />
</span>
</template>
<script setup lang="ts">
const props = defineProps<{
variant: 'success' | 'error' | 'warning' | 'info'
}>()
const classes = computed(() => {
const base = 'px-2 py-1 rounded text-xs font-medium'
const variants = {
success: 'bg-success/10 text-success',
error: 'bg-error/10 text-error',
warning: 'bg-warning/10 text-warning',
info: 'bg-info/10 text-info',
}
return `${base} ${variants[props.variant]}`
})
</script>
Вариант 2: Tailwind @apply (крайне редко)
Только для базовых утилит в глобальных стилях:
/* app/assets/css/base.css */
@layer components {
.btn-base {
@apply px-4 py-2 rounded font-medium transition-colors;
}
}
Не рекомендуется: @apply теряет преимущества atomic CSS.
Чек-лист
- Компонент не содержит
<style>блок - Все стили через Tailwind-классы
- Нет интерполяции в именах классов (
bg-${color}) - Длинные списки классов отформатированы
- Динамические классы через
:classbinding с полными именами - Используются
group/peer/hasвместо JS для связанных состояний - Переиспользуемые элементы вынесены в компоненты