Зоопарк CSS технологий
Ilya YurkinСтилизациия приложений очень холиварная тема, особенно в React. Я подготовил обзор решений с которыми успел поработать, в той или иной степени. Половина из них применимы, в основном, к React, а другая половина не ограничена библиотеками или фреймворками. Цель этой статьи не рассказывать подробно о каждом подходе, а дать краткое описание и подсветить преимущества и недостатки.
Inline стили

Я бы советовал использовать инлайн стили только в крайних случаях. Например, когда значение стиля вычисляется на основе внешних данных, которые проблематично определить в css заранее: верстка вложенных папок, где каждый вложенный элемент смещается на константу относительно родителя.

Плюсы
- Быстро пишутся;
- Можно использовать JS для вычислений.
Минусы
- Не удобно преобразовывать обычный css в объект стилей;
- Мешанина разметки и стилей
- Нельзя делать анимации, использовать селекторы и прочие, обычные для css, фичи;
- Сложно масштабировать.
Обычный CSS
"Голый" CSS в наши дни почти нигде не встречается. Когда говорят о CSS, то подразумевают SASS/SCSS/LESS препроцессоры, да еще и с соглашениями типа БЭМ в придачу. У чистого CSS много фундаментальных проблем, чтобы использовать его "как есть". Я вынес SASS/SCSS и БЭМ в отдельные блоки, нет смысла разбирать обычный CSS.
Плюсы
- Естественно, все прелести современного CSS: селекторы, анимации и т.д.;
- Избавляет компонент от инлайн стилей.
Минусы
- Глобальное пространство имен (конфликты, коллизии);
- Сложно искать мертвый код;
- Бойлерплейт;
- Отображение не всегда ожидаемое из-за каскада. Подробнее про каскад тут.
SASS / SCSS
SASS/SCSS - это CSS на стеройдах. Препроцессоры добавляют синтаксический сахар вложений, переменные и функции. Вот небольшой пример:


Удобно, не правда ли? Вот только SASS/SCSS решает одну проблему из обычного CSS - бойлерплейт. Часто цена этого решения - размер конечного CSS файла. Я специально добавил .disabled для примера на 12 строке в SCSS коде. Как вы заметили этот .disabled класс добавился для обеих кнопок с одним и тем же значением (второй скрин). А теперь представьте, что вместо этого маленького класса добавили несколько больших статичных классов или миксинов. В конечном CSS файле будут сотни или даже тысячи строк стилей, которые можно было бы скомпоновать.
Плюсы
- Все плюсы обычного CSS;
- Динамические функции и переменные;
- Меньше бойлерплейта;
Минусы
- Все еще актуальна проблема глобального пространства имен;
- Мертвый код стало сложнее искать. Например, .button-40px класс не найти в CSS файлах обычным поиском по проекту. Нужно понимать, где и как он генерируется;
- Размер конечного CSS файла;
CSS/SCSS + БЭМ
БЭМ - это соглашение по созданию имен CSS классов, а не препроцессор или библиотека. Это соглашение позволяет сильно сгладить минусы, перечисленные в разделе обычного CSS, однако, не решить их полностью.
Конвенции упираются в человеческий фактор и поэтому проблемы CSS только сглаживаются. Разработчик поленился, ревьюверы не доглядели, и вот, код не по БЭМ попал в боевую версию. Иногда, он может стать бомбой замедленного действия.
Философия БЭМ решает проблему глобального пространства имен, а так же уменьшает потребность в примесях (mixins) и итераторов SCSS, за счет которых и раздувался код.
Я бы рекомендовал ознакомиться с документацией БЭМ, чтобы иметь представление как поддерживать проект, в котором присутствует конвенция имен. Сейчас существует достаточное количество проектов на SCSS + БЭМ, особенно за пределами React. И это количество продолжает расти. Это тот минимум, на котором действительно можно написать проект средних размеров и не испытывать боль и проблемы при стилизации.
В больших же проектах БЭМ может давать сбои и доставлять проблемы. Например, две разные команды могут создать компонент с одинаковым классом. Если же команды синхронизировались, то начинается головоломка: "Как же семантически назвать новый компонент, чтобы он не конфликтовал с другими похожими?". Если ваш проект на React, то существуют инструменты для решения и этих проблем.
Плюсы
- Проблема глобального пространства почти не актуальна;
- Самодокументируемый код, благодаря классам, привязанным к контексту.
Минусы
- Коллизия имен все еще существует, хоть и минимально;
- Имена классов могут стать очень огромными;
- В крупных проектах придумывание нового класса становится квестом;
- Излишняя семантичность.
CSS модули
CSS модуль — это CSS файл, в котором все имена классов и анимаций имеют локальную область видимости по умолчанию. Иными словами CSS модули - почти автоматизированный БЭМ. "Почти" - потому что разработчикам все еще нужно продумывать архитектуру стилей и решать конфликты классов, но уже на локальном уровне.

Как это работает? При импортировании CSS модуля все классы превращаются в поля объекта. Благодаря хэшированию каждый класс остается уникальным в глобальной области видимости. Если бы вы захотели создать компонент header, в котором есть своя кнопка button, то в src/header/styles/modules.css можно объявить такой же класс .button {...} и он не будет пересекаться и конфликтовать с классом из другого компонента.
Возможно, вы подумали о том, что из-за хэшированных классов будет сложнее читать DOM. Эта проблема легко решается настройками Webpack'а. Можно настроить отображение полного пути до компонента в имени класса в DOM и найти нужный класс не составит труда.
Плюсы
- Локальная область видимости, которая гарантирована технически;
- Нет коллизий и конфликтов имен;
- Удобные и короткие семантические имена классов;
- Явные зависимости. Удалять мертвый код очень просто;
- Поддержка SCSS/SASS.
Минусы
- Немного сложнее читать классы в DOM;
- Я не придумал ничего весомого, что можно было бы приписать к минусам CSS модулей. Возможно, для кого-то будет необычно использовать :global {} для объявление глобальных стилей, т.к. такого синтаксиса нет в обычном CSS.
CSS-in-JS
Подобно тому, как React позволил нам писать HTML как JavaScript с помощью JSX сахара, CSS-in-JS сделал нечто подобное с CSS. Это позволяет нам писать правила CSS стилей без создания .css файлов, и эти правила имеют локальную область видимости. Styled components - один из самых популярных представителей CSS-in-JS. Еще отдельно хотел бы выделить linaria, но о ней чуть позже.
Подробное сравнение популярных библиотек можно найти здесь.

CSS-in-JS - избавляют разработчиков от порой страшного нагромождения условий для классов. Примерно вот так выглядит стилизация в реальных проектах на БЭМ:

Большинство CSS-in-JS библиотек собирают стили в рантайме на клиенте, но не linaria. Linaria собирает стили во время билда проекта и на клиент отдается обычный css. Динамика в стилях достигается за счет css переменных. Цена этому - гибкость и функциональность. В linaria компонентах не получится использовать JS на всю мощь.
Плюсы
- Во время стилизации доступна вся мощь JS;
- Предсказуемость, локальная область видимости;
- Хороший Developer Experience. Не нужно придумывать ни имена классов, ни их селекторы.
Минусы
- Использование сторонних библиотек;
- Могут быть проблемы с производительностью на больших страницах (если стили собираются в рантайме на клиенте);
- Стили не кэшируются (если они собираются в рантайме);
- Проблематично делать семантическую верстку. Из названия компонента далеко не всегда понятно на каком HTML тэге он создан;
- Читать классы в DOM и сопоставлять их с кодом еще сложнее, чем c CSS модулями.
Tailwind и CSS фреймворки
Еще пару слов о tailwind. Посыл этого раздела будет справедлив и для других css фреймворков, в том числе, и для bootstrap сеток.
Tailwind тоже решает проблемы инлайн стилей и обычного CSS, но иначе. В отличие от БЭМ, который предлагает делать абстрактные классы, tailwind добавляет утилитарные классы. Стиль style={{ display: 'flex' }}
заменяется классом 'flex' и т.д.
Для каждого css свойства (или даже групп свойств) существует свой отдельный класс. В отличие от инлайн стилей css фреймворки более гибкие. Они позволяют делать и анимации, и адаптивную верстку.

Обратите внимание, имена классов уже длинные, а ведь это простой пример. При реализации анимации или адаптивной верстки - колбаса из классов гарантирована. Да, в теории можно создать абстрактный css класс и применить утилитарные классы через директиву @apply, но чем такой подход принципиально отличается от тех же примесей в SCSS?
Проблема глобальной области видимости здесь не стоит, так как нет завязки на абстракциях. Утилитарный класс будет применять свой css стиль вне зависимости от места использования. Но отказ от абстракций приносит и свои минусы. Поиск по проекту затрудняется, например, класс "flex p-6 font-mono" может быть применен ко множеству компонентов или вообще составляться на основе данных/состояний компонента.
const className = clsx('flex', { 'p-6': isLast }, 'font-mono')
Я бы рекомендовал перед внедрением tailwind согласовать ваше решение с дизайнерами, иначе придется спорить об отступах и менять конфиг достаточно часто, что снижает смысл использования фреймворка. Когда дизайнеры следуют философии tailwind'а, то верстать, действительно, становится в радость, не нужно высчитывать цвета, пиксели для отступов и т.д..
Плюсы
- Нет привязки к библиотеке или фреймворку;
- Упрощает и ускоряет процесс верстки, если вы с дизайнерами на одной волне;
- Компактный конечный CSS файл.
Минусы
- Сложно читать классы в DOM и сопоставлять их с кодом;
- Разметка и стили в одном файле;
- Набор классов может занимать несколько строк;
- Не все классы трактуются очевидно, особенно, если есть кастомные настройки.
Вывод
В качестве вывода хотел бы показать область применения каждого из подходов:
- Inline стили. Учитывая все минусы, я рекомендую использовать их в комбинации с любым другим подходом для вычисляемых значений, которые проблематично описать в css;
- Обычный CSS. Без БЭМ или CSS модулей адекватно можно сверстать, разве что, лабу в универе или пример в codeopen;
- SASS/SCSS. Аналогично пункту выше. Не рекомендую использовать без БЭМ или CSS модулей;
- CSS/SCSS + БЭМ. Классика. То, с чем можно построить приложение маленьких и средних размеров. На любом стэке. В больших приложениях растет риск коллизий имен и их решение превращается в квест. Для проектов на React есть варианты получше;
- CSS модули. Золотая середина для приложений на React. Настроить под них вебпак не составляет труда, а в CRA вообще работают из коробки. Можно строить приложения любых размеров, используя все прелести css, не ломать голову над именами классов и не бояться за глобальную область видимости;
- CSS-in-JS. Не стоит использовать для сайтов, в которых важен SEO, по крайней мере, run-time решения. Для маленьких проектов слишком много лишнего придется тянуть в проект. Хорошо подходят для средних и больших проектов и команд. В моей практике и styled-components и linaria хорошо показали себя на больших проектах-микрофронтах с UI китом.
- Tailwind. Может хорошо себя показать в проектах любого размера и назначения. Должен показывать хороший SEO результат. Tailwind подойдет, если вы: синхронизированы с дизайнерами, готовы отказаться от абстрактных классов и длинные имена классов вам не режут глаз. Если нет - выберете что-то другое.
- Bootstrap. Подойдет для мелких проектов, прототипов и всяких админок и дашбордов, на которые не выделили дизайнера или не хватило бюджета. В остальном может принести больше вреда, чем пользы.