Компонентный CSS против утилитарного
Спору десять лет. Разбираем оба подхода на одном интерфейсе, со счётчиком строк, и находим границу, где каждый выигрывает.
Что ты научишься делать
- Соберёшь один блок двумя способами и увидишь разницу в числах
- Поймёшь, какую задачу на самом деле решает Tailwind
- Выберешь гибрид, который работает в большинстве проектов
#Два ответа на один вопрос
Вопрос простой: где живёт оформление — в CSS или в разметке.
Компонентный подход: в разметке имя сущности, в CSS её описание. class="card" — а как выглядит карточка, написано в стилях.
Утилитарный подход: в разметке набор атомарных классов, каждый отвечает за одно свойство. class="p-4 bg-white rounded-xl" — и никакого CSS писать не нужно.
#Что реально решает утилитарный подход
Tailwind популярен не потому, что «модно». Он закрывает четыре настоящие боли, знакомые каждому, кто вёл проект дольше полугода.
| Боль | Как её решают утилиты |
|---|---|
| CSS растёт и никогда не уменьшается | Новый блок не добавляет ни строки стилей |
| Страшно удалять правила: непонятно, где используются | Удаляешь разметку — оформление уходит вместе с ней |
| Час на придумывание имени для обёртки | Имена не нужны вообще |
| Значения расползаются: то 13px, то 14px | Доступны только шаги шкалы |
#Чем за это платят
- Разметка перестаёт читаться. Двенадцать классов на элементе прячут структуру. Чтобы понять, что это карточка товара, приходится вглядываться.
- Повторение. Двадцать карточек — двадцать одинаковых наборов классов. Поменять все разом уже нельзя: нужно править каждую или заводить компонент.
- Нужна сборка. Полный набор утилит весит много; в проект попадают только использованные — а значит, требуется инструмент, который их вычислит.
- Свой язык.
rounded-xlиgap-2надо знать. Обычный CSS знают все.
Так не надо
<!-- Что это за блок? Придётся вчитываться -->
<div class="flex items-center gap-3 p-4 rounded-xl
bg-white shadow-sm hover:shadow-md transition
border border-gray-200 dark:bg-gray-800">Оформление видно сразу, а смысл — нет. И так на каждом элементе страницы.
Так надо
<!-- Сразу понятно, что это -->
<article class="product-card">Разметка описывает содержание. Оформление лежит там, где ему место, — в стилях.
#Гибрид, который обычно и получается
На практике команды редко выбирают крайность. Работающее правило деления такое.
Компоненты — для сущностей. Всё, у чего есть название в предметной области: карточка товара, шапка, форма заказа.
Утилиты — для раскладки и мелких правок. Отступ между блоками, выравнивание, скрытие на мобильном. То, что не является частью сущности и меняется от места к месту.
<!-- Утилиты снаружи: расстояния задаёт контейнер --><section class="stack gap-6"> <!-- Компонент внутри: сущность со своим именем --> <article class="product-card"> <h3 class="product-card__title">Раф</h3> <p class="product-card__price">240 ₽</p> </article> <!-- Утилита для разовой правки, без нового класса --> <p class="text-center">Показать ещё</p></section>#Утилиты своими руками
Чтобы получить пользу от утилит, фреймворк не обязателен. Двадцать классов покрывают девяносто процентов случаев, и их можно написать руками за полчаса.
@layer utilities { /* Раскладка */ .stack { display: grid; } .row { display: flex; align-items: center; } .row--between { justify-content: space-between; } /* Расстояния — по шкале, произвольных чисел нет */ .gap-1 { gap: var(--space-1); } .gap-2 { gap: var(--space-2); } .gap-4 { gap: var(--space-4); } .gap-6 { gap: var(--space-6); } /* Текст */ .text-center { text-align: center; } .text-muted { color: var(--color-muted); } .text-sm { font-size: .875rem; } /* Состояния видимости */ .is-hidden { display: none; } .visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }}#Что происходит с файлом за год
Разница между подходами лучше всего видна не на одной карточке, а на графике роста.
Компонентный CSS растёт примерно линейно: каждый новый блок добавляет свои правила. Утилитарный выходит на плато: после первой сотни классов новые почти не появляются, потому что набор уже покрывает нужды.
| Через сколько | Компоненты | Утилиты |
|---|---|---|
| Месяц | ~600 строк CSS, разметка читается | ~400 строк набора, разметка шумная |
| Полгода | ~2500 строк, часть уже не используется | тот же набор, разметка так же шумная |
| Год | ~5000 строк, десятая часть мёртвая | набор почти не вырос |
| Удалили раздел | правила остались, тронуть боятся | оформление ушло вместе с разметкой |
#Как это выглядит в реальном проекте
@layer reset, base, layout, components, utilities;@layer components { .product-card { display: grid; gap: var(--space-2); padding: var(--space-4); background: var(--color-surface); border-radius: var(--radius-md); }}@layer utilities { /* Побеждает компонент по определению, без !important */ .p-0 { padding: 0; } .text-center { text-align: center; }}#Когда НЕ надо
- Утилиты без сборки на большом сайте. Полный набор весит сотни килобайт. Без инструмента, вырезающего неиспользуемое, это чистый вред.
- Компоненты для одноразового блока. Если блок встречается один раз и больше не появится, класс с именем — лишняя сущность. Хватит утилит.
- Смешивать без правила. Хуже обоих подходов — когда часть оформления в компоненте, часть в утилитах, и никто не знает, где искать. Договорённость важнее выбора.
- Спорить об этом дольше часа. Оба подхода рабочие. Проект гораздо чаще умирает от отсутствия договорённости, чем от неправильного выбора.
Где спотыкаются почти все
Утилиты с !important на каждом правиле
Почему Так делали до слоёв каскада. Теперь важность мешает: перекрыть утилиту в исключительном случае становится нечем.
Как надо Положи утилиты в последний слой @layer utilities — они победят и без важности.
Компонент со ста утилитами внутри
Почему Разметка перестаёт читаться, а изменить все карточки разом уже невозможно.
Как надо Повторяется больше двух-трёх раз — заводи компонент.
Утилиты с произвольными значениями
Почему Классы вроде .mt-13 убивают главную пользу шкалы: значения снова расползаются.
Как надо Только шаги шкалы. Нужно другое значение — это повод для компонента.
Дублирование: класс компонента и те же утилиты рядом
Почему Два источника правды. При правке компонента результат не меняется, потому что утилита сильнее.
Как надо Реши, кто отвечает за это свойство, и убери второе.
- Компонентный CSS component CSS
- Подход, где класс называет сущность, а оформление живёт в стилях.
- Утилитарный CSS utility-first CSS
- Подход, где каждый класс отвечает за одно свойство, а оформление живёт в разметке.
- Атомарный класс atomic class
- Класс из одного объявления:
.text-center,.gap-2. - Мёртвый CSS dead CSS
- Правила, которые больше нигде не применяются, но их боятся удалить.
Проверь себя
Разметку удалить не страшно. Правила в компонентном CSS остаются, и со временем никто не решается их трогать.
Со слоями важность больше не нужна: достаточно положить утилиты в последний слой.
Вопрос не в размере, а в принадлежности: скругление — часть карточки, расстояние между карточками — нет.
Смысл шкалы в ограничении выбора. Как только появляются произвольные значения, возвращается прежний хаос.
Оба подхода рабочие. Проблемы начинаются, когда правила деления нет и каждый решает по-своему.
Задание
Собрать блок двумя способами
Слева карточка на утилитах, справа — место под компонентную версию.
Сделай две вещи: заведи слои @layer components, utilities; собери класс .card со всем оформлением и повесь его на вторую карточку вместо утилит. Утилиту .text-center оставь работающей — она должна перекрывать компонент без !important.
Подсказка 1
Первой строкой: @layer components, utilities;
Подсказка 2
Компонент: @layer components { .card { display: grid; gap: 8px; padding: 16px; background: #f4f4f8; border-radius: 12px; text-align: left; } }
Подсказка 3
Все существующие утилиты заверни в @layer utilities { … } — тогда .text-center победит без важности.
Показать решение
<article class="p-4 bg-surface rounded-md gap-2 grid"> <h3 class="m-0 text-md">На утилитах</h3> <p class="m-0 text-sm text-muted">Оформление в разметке</p></article><article class="card text-center"> <h3>Компонентная</h3> <p>Здесь должен быть класс card</p></article>@layer components, utilities;@layer components { .card { display: grid; gap: 8px; margin-top: 12px; padding: 16px; background: #f4f4f8; border-radius: 12px; text-align: left; } .card h3 { margin: 0; font-size: 16px; } .card p { margin: 0; font-size: 14px; color: #5b6070; }}@layer utilities { .p-4 { padding: 16px; } .bg-surface { background: #f4f4f8; } .rounded-md { border-radius: 12px; } .gap-2 { gap: 8px; } .grid { display: grid; } .m-0 { margin: 0; } .text-md { font-size: 16px; } .text-sm { font-size: 14px; } .text-muted { color: #5b6070; } .text-center { text-align: center; }}