Tailwind отлично подходит для прототипов. Но когда проект растёт, его основная концепция становится именно той причиной, по которой ваша дизайн-система разваливается.
Недавно в посте на LinkedIn я написал: «Нельзя построить жизнеспособную дизайн-систему на основе фреймворка Tailwind CSS». И вот почему.

Если взять любой классический CSS-фреймворк и правильно всё настроить, можно делать довольно сложные вещи очень легко. Например, в каждой дизайн-системе есть несколько типов кнопок: primary, secondary и т.д. Но что происходит, когда нужно добавить новую вариацию?
Новый вариант кнопки
Допустим, вам нужна новая кнопка btn-focus. Но её нужно интегрировать в существующую дизайн-систему. То есть это должна быть не просто кнопка с другим цветом фона, а кнопка, которую можно комбинировать с существующими классами дизайн-системы: btn-sm, btn-lg, btn-icon, btn-outline и т.д.
Помимо этого, в дизайн-системе обычно ты определяешь чёткие правила того, как работают дополнительные состояния. Например, цвет фона всех кнопок в состоянии hover должен быть на 15% темнее основного цвета в нормальном состоянии. Или цвет текста в активном состоянии для всех кнопок должен быть на 10% светлее.
Во многих классических UI-фреймворках можно найти что-то вроде:
.button-variant-other(@color; @background; @border);
Это генерирует полностью интегрированный вариант кнопки с помощью одной функции. Она позволяет задать систему того, как будут работать разные варианты кнопок и разные состояния этих вариантов. При этом новые элементы будут интегрированы в дизайн-систему UI-фреймворка.
Можно ли сделать то же самое легко в Tailwind? Сомневаюсь.
После моего поста на LinkedIn я получил несколько возможных решений и хотел бы их рассмотреть и прокомментировать тут.
1. CVA решение
И в качестве примера — как Shadcn структурирует код компонента Button.
Давайте посмотрим:
variant "default" | "outline" | "ghost" | "destructive" | "secondary" | "link" "default" size "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg"
У них есть несколько вариантов кнопок и несколько размеров. Выглядит чисто и просто? Да. И я могу сказать, что это основной подход для Tailwind CSS прямо сейчас. Главное преимущество — скорость разработки. Поскольку не нужно переключаться между файлом компонента и файлом стилей, код пишется значительно быстрее. В чём недостаток?
Ограниченное количество комбинаций, которые можно использовать, и этого недостаточно для долгосрочного проекта. Как только вам понадобится
primaryoutlinedsmallкнопка, вы столкнётесь с таким ограничением.
Не каждому проекту нужна сложная дизайн-система. Многим она вообще не нужна. С точки зрения бизнеса скорость и стоимость разработки гораздо важнее. Но когда вы работаете над большим проектом продолжительностью более 6 месяцев, такое решение превратится в технический долг, который будет вас тормозить в будущем, увеличивая и время и стоимость разработки.
2. Подход через CSS-переменные
.btn { /* базовый класс */ @apply inline-flex items-center justify-center rounded px-4 py-2 font-body; /* используем переменную для определения состояний */ background-color: var(--btn-color); color: var(--btn-text, white); /* по умолчанию белый текст */ } .btn:hover { /* на 15% темнее для любого цвета */ background-color: color-mix(in srgb, var(--btn-color), black 15%); } .btn:active { /* на 10% светлее */ filter: brightness(1.1); } /* варианты задают конкретные цвета */ .btn-primary { --btn-color: theme('colors.blue.600'); } .btn-whatever { --btn-color: … }
Лично мне этот подход нравится. Он выглядит элегантно и эффективно. Основная идея — менять значение переменной внутри варианта кнопки вместо того, чтобы переписывать стиль.
Однако такое решение не связано конкретно с фреймворком Tailwind. Его можно реализовать на чистом CSS и использовать с любой UI-библиотекой. Если же вы выбираете Tailwind CSS при таком подходе, то вы теряете его главное преимущество — скорость, потому что не можете писать стили прямо в файле компонента.
3. Директива @utility
@utility btn-* { font-size: --value(--text-ui-*, [length]); background-color: --value(--color-ui-*, [color]); color: --value(--color-ui-*-text, [color]); }
Ещё одно интересное решение. Оно создаёт утилитарные классы по определённым правилам, после чего можно использовать CSS-переменные с заданными именами для кастомизации этих вариантов.
Таким образом, можно переиспользовать классы вместе с модификаторами Tailwind: для разных состояний или адаптивного дизайна. Но использовать btn-focus класс для средних или маленьких разрешений не имеет особого смысла — btn-primary или btn-focus функциональные классы.
Функция utility, даже если выстроить вокруг неё систему, — это лишь костыль. Да, можно создать 5 основных типов кнопок на её основе и даже быстро добавлять новые. Но что насчёт других типов или комбинаций этих типов между собой?
Допустим, нужно создать кнопки разных размеров или тип outline кнопки, который можно комбинировать с основными типами. Как, например, задать стили для primary small кнопки с outline рамкой? Можно создать вариации в компоненте кнопки и попытаться кастомизировать там, но вся система становится очень хрупкой.
Заключение
В конце хочу добавить, что Tailwind как утилитарный фреймворк не навязывает конкретный подход к построению дизайн-системы — он предполагает, что вы выстроите собственное решение. Однако в его основе лежит вполне конкретная концепция: компоненты служат в качестве CSS-классов, а CSS-классы служат в качестве CSS-свойств. С учётом этого CVA выглядит как единственное правильное, но очень ограниченное решение для использования с Tailwind. Или вы всегда можете выбрать что-то более подходящее.
