Перейти к содержимому

Utility-first помимо Tailwind: UnoCSS, daisyUI и уроки предшественников

Tailwind — доминирующий utility-first фреймворк, и его глубокий разбор живёт в разделе 05: Tailwind CSS глубоко. Но Tailwind не появился из ниоткуда и не остался один. Его идея — атомарные классы вместо семантических — восходит к Tachyons 2014 года, а рядом с ним в 2026-м живут UnoCSS с принципиально другой архитектурой генерации и daisyUI, которая добавляет над Tailwind слой готовых компонентов. Плюс WindiCSS — предостережение о том, как быстро инструмент умирает, когда строится на чужой инфраструктуре.

Почему это важно: знание одного фреймворка делает тебя его пользователем, знание всей экосистемы — инженером, способным выбрать инструмент под задачу и не удивляться чужому коду. В этой главе разбираем, чем UnoCSS принципиально отличается от Tailwind архитектурно, как daisyUI превращает «классы-атомы» обратно в «компоненты», что гибридные схемы делают с headless-библиотеками, и по каким критериям выбирать между всем этим для pet-проекта и для SaaS с дизайн-системой.

Короткая история: от Tachyons к современности

Заголовок раздела «Короткая история: от Tachyons к современности»

Tachyons (2014) — прадед жанра. Адам Морс собрал набор атомарных классов (pa2 = padding-all 0.5rem, flex, bg-blue) и доказал идею: можно верстать всё, комбинируя маленькие классы, и получать крошечный CSS за счёт переиспользования. Ограничения Tachyons: фиксированная шкала, ни конфига, ни JIT — каждый проект получал весь набор утилит.

Basscss, Shed.css, ACSS (Atomizer) — середина 2010-х, те же идеи: атомарность, вёрстка через классы. ACSS от Yahoo даже генерировал атомы on-demand из классов в разметке — но остался нишевым.

Tailwind (2017) — Адам Ватан объединил идею атомарности с полной конфигурируемостью и дизайн-токенами: шкалы цветов, отступов, типографики как данные в tailwind.config.js. С версии 3.0 (2021) — JIT-компилятор: сканирует исходники и генерирует только используемые утилиты. История успеха известна.

WindiCSS (2020–2023) — первый популярный «JIT-альтернативный» движок, совместимый с Tailwind-синтаксисом, но быстрее за счёт пропуска PostCSS-цепочки. Дальше — история-предостережение, разберём ниже.

UnoCSS (Энтонью Фу, 2021) — не «ещё один набор утилит», а движок генерации: ядро не знает ни одного CSS-класса. Все утилиты приходят из пресетов — плагинов, которые декларируют правила вида «класс по regex → CSS-объявления». Три строчки из документации объясняют идею:

unocss.config.ts
import { defineConfig, presetUno, presetAttributify, presetIcons } from 'unocss';
export default defineConfig({
presets: [
presetUno(), // superset: правила, совместимые с Tailwind/Windi/Bootstrap-утилитами
presetAttributify(), // атрибутный синтаксис (см. ниже)
presetIcons(), // 100k+ иконок как CSS-классы
],
// свои правила — обычные объекты/regex:
rules: [
['text-brand', { color: '#7c3aed' }],
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })], // m-1 = .25rem
['card-shadow', { 'box-shadow': '0 2px 8px rgb(0 0 0 / 8%)' }],
],
shortcuts: {
// алиасы-композиции: один класс = набор утилит
btn: 'px-4 py-2 rounded-lg font-medium transition-colors',
'btn-primary': 'btn bg-violet-600 text-white hover:bg-violet-700',
},
});

Механика под капотом отличается от Tailwind радикально. Tailwind (v3+) — это JIT-компилятор, который при старте dev-сервера или сборке сканирует файлы по content-путям, находит классы, генерирует CSS и складывает в один бандл. UnoCSS — runtime-трансформация по требованию: каждый класс из разметки пропускается через матчинг правил в момент обращения, без полного сканирования проекта. На практике это даёт:

  • старт dev-сервера мгновенный (нет фазы сканирования);
  • генерация утилит по ходу — можно динамически собирать классы строкой и они всё равно попадут в CSS (в Tailwind это требует safelist);
  • лёгкость кастомизации: своё правило — три строки в конфиге, а не плагин с AST-трансформами.
Окно терминала
npm i -D unocss
vite.config.ts
import UnoCSS from 'unocss/vite';
export default {
plugins: [UnoCSS()],
};
// main.ts — импорт сгенерированного CSS
import 'virtual:uno.css';

Фирменная фича UnoCSS: утилиты можно писать атрибутами, а не классами. Разметка становится чище, визуально группируется по смыслу:

<!-- Одинаковое: классная запись -->
<button class="px-4 py-2 rounded-lg bg-violet-600 text-white hover:bg-violet-700">
Сохранить
</button>
<!-- И attributify-запись -->
<button
px-4 py-2 rounded-lg
bg-violet-600 text-white
hover:bg-violet-700
>
Сохранить
</button>

Плюсы: разметку проще сканировать глазами (группы атрибутов), меньше «классовой простыни». Минусы: JSX/TS не типизируют произвольные атрибуты без дополнительной работы; некоторые линтеры ругаются; при миграции с Tailwind — новый синтаксис для команды. В UnoCSS это опциональный пресет, можно держать классный стиль.

Критерий Tailwind CSS 4 UnoCSS
Архитектура Компилятор: сканирование + генерация бандла Движок: трансформация по требованию
Старт dev-сервера Сканирование content (секунды на большом проекте) Мгновенный, генерация lazy
Конфигурация theme-объект + плагины Пресеты + правила-объекты
Кастомные утилиты Плагин через matchUtilities/CSS Правило за 3 строки
Динамические классы Требуют safelist Работают из коробки
Экосистема Огромная: плагины, UI-киты, daisyUI Растущая: пресеты, интеграции
Компонентный слой Headless (Radix) + cva Аналогично, shortcuts
CSS-in-JS интеграции tailwind-merge, cva uno-merge, аналоги
Зрелость/поддержка Индустриальный стандарт Активна, меньшая комьюнити
Лицензия MIT MIT

Практический вывод: для команды и долгого продукта — Tailwind как стандарт с максимальной экосистемой. Для экспериментов, дизайн-систем с нестандартными токенами, библиотек-авторов — UnoCSS даёт гибкость, которой у Tailwind нет без борьбы с конфигом.

daisyUI (Поур Сааде) решает обратную задачу: Tailwind даёт атомы, а разработчику часто нужны готовые молекулы. daisyUI — это набор готовых классов-компонентов (btn, card, modal, navbar), построенных на Tailwind-утилитах и темизируемых через CSS-переменные.

Окно терминала
npm i -D daisyui
tailwind.config.js
module.exports = {
plugins: [require('daisyui')],
daisyui: {
themes: ['light', 'dark', 'corporate'], // готовые темы
darkTheme: 'dark',
},
};
<!-- Компонент из атомов без единой строчки своего CSS -->
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Метрики</h2>
<p>Выручка выросла на 12%.</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">Открыть</button>
</div>
</div>
</div>
<!-- Кнопка с состояниями и темизацией из коробки -->
<button class="btn btn-outline btn-error btn-sm">Удалить</button>
<!-- Модалка нативная, управляемая без JS -->
<button class="btn" onclick="my_modal.showModal()">Открыть</button>
<dialog id="my_modal" class="modal">
<div class="modal-box">
<h3>Подтверждение</h3>
<div class="modal-action">
<form method="dialog"><button class="btn">Закрыть</button></form>
</div>
</div>
</dialog>

Механика темизации: daisyUI генерирует набор семантических CSS-переменных (--p = primary, --pc = primary-content, --b1 = base-100 и т.д.), а каждый компонент ссылается на них. Переключение темы = замена набора переменных на [data-theme="..."]. Добавить свою тему:

daisyui: {
themes: [
{
mybrand: {
primary: '#7c3aed',
'primary-content': '#ffffff',
secondary: '#f59e0b',
'base-100': '#fafafa',
// семантические слоты: primary/secondary/accent/neutral + base-100/200/300 + info/success/warning/error
},
},
],
}

Когда брать daisyUI:

  • Нужны компоненты, но не нужен уникальный дизайн — внутренние инструменты, MVP, прототипы: скорость Bootstrap, но на современном стеке Tailwind.
  • Нужна мультитемность из коробки (светлая/тёмная/брендовые) без построения дизайн-системы.
  • Не нужен полный контроль над каждым пикселем: daisyUI-компоненты кастомизируются, но их структура навязывает свои классы.

Когда нет: продукт с собственным брендом и дизайн-системой (тогда headless-примитивы + свои утилиты, см. Radix/Shadcn), либо требования к pixel-perfect, которые конфликтуют с дефолтными стилями daisyUI.

WindiCSS взлетел в 2020–2021 на совместимости с Tailwind v2-синтаксисом и скорости: обход PostCSS, нативная интеграция с Vite, фичи вроде variant groups (hover:(bg-red-500 text-white)). В 2023 году разработка остановилась: в пользу чего?

  • Tailwind v3 встроил JIT как дефолт — главное преимущество Windi (скорость генерации) перестало быть уникальным.
  • Совместимость ломалась с каждым релизом Tailwind: синтаксис-«superset» обязан бежать за апстримом, а ресурсов у проекта с одним основным мейнтейнером не хватало.
  • Экосистема не успела вырасти: плагины, UI-киты, гайды писали под Tailwind.

Урок для выбора инструментов: «совместимый с X» ≠ «X». Инструмент-надстройка живёт, пока жив апстрим и пока его преимущество не поглощено. Ставить в долгоживущий продукт зависимость от нишевого мейнтейнера — риск, который надо осознавать. Сегодня WindiCSS встречается только в legacy, и миграция с него на Tailwind или UnoCSS — механическая (синтаксис близок).

Самая популярная схема 2024–2026 для продуктов с дизайн-системой: headless-примитивы дают поведение и доступность, Tailwind — стилизацию. Это та самая архитектура Shadcn:

// Компонент = Radix-примитив (доступность, клавиатура, focus trap)
// + Tailwind-классы (визуал)
// + cva (варианты)
import * as Dialog from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-violet-600 text-white hover:bg-violet-700',
outline: 'border border-violet-600 text-violet-600 hover:bg-violet-50',
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' },
},
defaultVariants: { variant: 'primary', size: 'md' },
}
);
export function ConfirmDialog() {
return (
<Dialog.Root>
<Dialog.Trigger className={buttonVariants()}>Удалить</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/40" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-xl bg-white p-6 shadow-xl">
<Dialog.Title className="text-lg font-semibold">Точно удалить?</Dialog.Title>
<Dialog.Description className="mt-2 text-sm text-slate-600">
Действие необратимо.
</Dialog.Description>
<div className="mt-4 flex justify-end gap-2">
<Dialog.Close className={buttonVariants({ variant: 'outline' })}>
Отмена
</Dialog.Close>
<button className={buttonVariants({ variant: 'danger', size: 'sm' })}>
Удалить
</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

Разбор радикальных примитивов, asChild, cn()-хелпера и философии Shadcn — в Radix UI и Shadcn, здесь важна роль слоёв: headless = логика и a11y, утилиты = визуальные токены, cva = вариативность без дублирования. Эта связка заменяет и CSS-in-JS по типизации, и компонентные библиотеки по контролю.

Что выбрать: pet-проект против SaaS с дизайн-системой

Заголовок раздела «Что выбрать: pet-проект против SaaS с дизайн-системой»
Критерий Pet-проект / портфолио SaaS с брендом и дизайн-системой
Главная цель Скорость разработки, обучение Консистентность, масштабирование, найм
Рекомендуемый стек Tailwind + daisyUI (MVP-скорость) или Bootstrap Tailwind + headless (Radix/Shadcn) + cva
Если хочется поэкспериментировать UnoCSS + attributify UnoCSS с кастомными пресетами под токены
Компонентный слой daisyUI или shadcn/ui shadcn/ui, Mantine, либо своя библиотека
Темизация daisyUI-темы или data-theme CSS-переменные + Tailwind-токены, контрактные токены
Риски Никаких, меняй сколько хочется Зависимость от нишевых инструментов (Windi-эффект), bus factor

Правило, которое работает почти всегда: для скорости — готовые компоненты (daisyUI, Bootstrap), для контроля — атомы + headless (Tailwind + Radix), для экспериментов — UnoCSS. И помни про связь с производительностью: размер утилитарного CSS растёт линейно с количеством уникальных классов, поэтому бюджеты из Core Web Vitals проверяются на реальном бандле.

  1. Генерация классов строкой без safelist в Tailwind. className={bg-${color}-500} не попадёт в сканер — класс не сгенерируется и стили пропадут. Решение в Tailwind: safelist или полные литеральные классы; в UnoCSS — работает из коробки, что частая причина переезда.
  2. «Утилиты для всего» включая сложные анимации и многослойные селекторы. Когда класс превращается в before:after:md:hover:bg-[...], читаемость умирает. Правило: утилиты для компоновки и типовых состояний, обычный CSS/CVA — для сложного.
  3. daisyUI + свои тяжёлые кастомизации поверх каждого компонента. Получается слоёный пирог: daisyUI-стили → твои перекрытия → !important. Если каждый компонент переписан, daisyUI не нужен — бери чистый Tailwind.
  4. Миграция с WindiCSS «на досуге». Совместимость ломается, зависимости не обновляются, уязвимости копятся. Мигрируй сразу на Tailwind v3+ или UnoCSS: синтаксис близок, механика — замена пресета/плагина.
  5. Смешение attributify и классного синтаксиса в одном проекте без договорённости. Читаемость падает, поиск по коду усложняется. Выбери один стиль на проект.
  6. Ожидание, что UnoCSS — дропин-замена Tailwind с той же экосистемой. Плагины Tailwind (typography, forms) имеют аналоги, но не порты; UI-киты писаны под Tailwind-классы и с UnoCSS требуют проверки (presetUno покрывает большинство, но не всё).
  7. Игнорирование content-путей в Tailwind при переезде проекта. После смены структуры папок сканер не видит новые файлы → половина стилей молча исчезает. Первое, что проверяешь при «сломавшихся стилях после рефакторинга».
  1. Чем UnoCSS архитектурно отличается от Tailwind? Tailwind — компилятор: сканирует content-файлы, генерирует бандл утилит на сборке. UnoCSS — движок трансформации по требованию: классы матчатся правилами-пресетами в момент обращения, без полного сканирования, что даёт мгновенный старт и динамическую генерацию.
  2. Что такое attributify-режим и зачем он нужен? Синтаксис, где утилиты пишутся HTML-атрибутами (<button px-4 bg-red-500>) вместо классов: разметка визуально группируется по смыслу. Фирменная фича UnoCSS (presetAttributify), опциональна.
  3. Как daisyUI темизируется? Что под капотом? Генерирует семантические CSS-переменные (primary, base-100, info…) для каждой темы, компоненты ссылаются на них; тема переключается через data-theme. Своя тема — объект цветов в конфиге.
  4. Почему WindiCSS умер и какой из этого урок? Tailwind v3 поглотил главное преимущество (JIT по умолчанию), а «совместимый superset» не успевал за апстримом при ресурсах одного мейнтейнера. Урок: зависимость-надстройка над чужим синтаксисом — риск; нишевый bus factor — риск; «дропин-совместимость» не заменяет экосистему.
  5. Динамические классы в Tailwind: почему bg-${color}-500 не работает и что делать? Сканер ищет полные литеральные строки; интерполяция не видна на этапе генерации. Решения: safelist в конфиге, маппинг объектом ({red: 'bg-red-500'}), полные классы в данных, либо движок с runtime-генерацией (UnoCSS).
  6. Когда daisyUI уместен, а когда — нет? Уместен: внутренние инструменты, MVP, мультитемность без дизайн-системы. Неуместен: продукт с уникальным брендом и pixel-perfect требованиями — тогда headless + свои токены.
  7. Что такое cva и какую проблему решает в связке Tailwind + headless? class-variance-authority: типизированные варианты компонента (variant × size) с дефолтами и compose. Решает проблему условных строк классов, даёт автокомплит и единый API вариативности.
  8. Как выбрать между Tailwind, UnoCSS и daisyUI для нового проекта? Команда и долгий продукт → Tailwind (экосистема, найм). Нестандартные токены/библиотечная разработка/эксперименты → UnoCSS. Нужна скорость с готовыми компонентами и темами → Tailwind + daisyUI или Bootstrap (см. предыдущую главу).
  1. Подними Vite-проект с UnoCSS: presetUno + presetAttributify + два кастомных правила (брендовый цвет, кастомная тень) и один shortcut (btn-primary). Критерий: компонент кнопки собран и классным, и атрибутным синтаксисом, CSS содержит только используемые утилиты.
  2. Воспроизведи граблю динамических классов: создай в Tailwind-проекте компонент с bg-${color}-500, убедись, что стили отсутствуют. Исправь тремя способами: safelist, маппинг, полные литералы. Критерий: все три варианта работают, разница задокументирована.
  3. Собери мини-дашборд на Tailwind + daisyUI: карточки, кнопки, таблица (если есть) с тремя темами (light/dark/своя). Критерий: переключение тем через data-theme меняет всю палитру без перезагрузки, своя тема определена через конфиг.
  4. Сравни бандлы: собери один и тот же лендинг на (а) Bootstrap-утилитах, (б) Tailwind, (в) UnoCSS. Замерь размер CSS в продакшене. Критерий: таблица с цифрами и объяснением различий (что генерируется, что тянется целиком).
  5. Рефакторни один компонент с условными строками классов на cva с вариантами variant/size и типизацией через VariantProps. Критерий: вызов с неизвестным вариантом — ошибка TypeScript, автокомплит работает.
  6. Спланируй миграцию условного проекта с WindiCSS на Tailwind v3: список несовместимостей (variant groups, shortcuts, конфиг), порядок шагов, критерии приёмки. Критерий: план, по которому можно мигрировать за один рабочий день.