Здравствуйте, товарищи!

Меня зовут Валентин, и сегодня мы снова поговорим про Atomic CSS! Обсудим имеющиеся проблемы в верстке и посмотрим, как атомарный подход их решает (или не решает). Разберем основные мифы, посмотрим на хорошие практики этого подхода и сделаем некоторые выводы.

Эту статью можно отчасти считать продолжением моей предыдущей по данной теме. Но если там был хардкор и технические детали, то здесь уже разберем прикладные вопросы: как верстать в Atomic CSS, чтобы получить заявленный эффект.

Это расшифровка моего доклада с FrontendConf 2024. Можете глянуть запись, а можете почитать эту статью с небольшими дополнениями и более выверенными формулировками.

Пара слов о себе

В IT около 10 лет. Последние годы занимался разработкой инструментов и бэкендом на Node.js, а сейчас делаю редакторы документов, типа Word и Excel. Разрабатываю свой open source проект. Выступаю на конференциях и веду IT-сообщество в Питере на 1000+ человек

Почему именно я буду рассказывать про Atomic CSS?

  • В теме с 2018, когда Tailwind еще был noname библиотекой

  • Смотрел все релевантные инструменты, у которых больше 20 звезд на Гитхабе

  • 3 года карьеры много верстал

  • В разработку своего инструмента вложил уже сильно больше 1000 часов

Проблемы верстки

БЭМ

Хотел бы начать разговор о проблемах верстки со своей истории. В 2018 году я достаточно много верстал и конкретно угорал по БЭМ-у. К маю того года я пересмотрел, наверное, все доклады и статьи на эту тему.

Олды тут?
Олды тут?

И когда вы только начинаете работать с БЭМ-ом, у вас возникают стандартные вопросы:

  • Блок это или элемент?

  • Делать микс или модификатор?

  • Как организовать структуру каталогов?

Дальше, когда вы с этим немного поработали, появляются вопросы более продвинутые:

  • Уровни переопределения

  • Как собирать много блоков без конфликтов?

  • Как автоматизировать создание сущностей?

Далее вы приходите к БЭМ-стеку, начинаете разбираться и в какой-то момент осознаете, что это сильно сложнее, чем нижние подчеркивания и дефисы, о которых нам рассказывали в самом начале.

Современные решения

CSS-in-JS
CSS-in-JS

Но сейчас уже далеко не 2018 год, и есть более современные подходы к написанию CSS

  • Scoped-решения

  • CSS-in-JS

  • Shadow DOM

Scoped-решения

Под Scoped-решениями я подразумеваю, например, CSS-modules, а также механизмы изоляции стилей, встроенные в такие фреймворки как Vue и Svelte. Да, эти инструменты помогают нам изолировать стили, но какие у них есть проблемы?

  • Пишем CSS как обычно. Казалось бы, это плюс, потому что у нас нет дополнительного порога вхождения. Но, возможно, стандартный способ написания стилей не самый эффективный. Ведь нам все так же требуется придумывать названия классов (хоть и чуть попроще) и писать все правила руками.

  • Переключение контекста между разметкой и стилями у нас все еще остается.

  • Не для каждого стека подойдут такие инструменты. Если у нас проект на Ruby или PHP с шаблонизатором, то тут мы их вряд ли сможем воткнуть.

  • Может увеличиваться размер CSS бандла, поскольку в таких инструментах стили редко переиспользуются. Но вероятно, есть способ улучшить ситуацию, если добавить в сборку аглификацию.

CSS-in-JS

Следующий из подходов - CSS-in-JS. Есть много библиотек вроде styled-components, StyleX, Emotion. Если у нас SPA с большим количеством динамики, то эти решения могут хорошо зайти. Но и у них имеются свои минусы:

  • Пишем CSS не как обычно. У нас повышается порог вхождения.

  • Свои ограничения и баги у разных библиотек.

  • Не для каждого стека, опять же.

  • Растет размер бандла при рантайм-решениях.

Под рантайм-решениями я подразумеваю такой классический CSS-in-JS. Когда у нас стили собираются в рантайме, но изначально, они лежат внутри строк в JS-бандле. Но уже современные библиотеки позволяют на этапе сборки перевести все наши стили из JS в реальный CSS.

Shadow DOM

Ну и про Shadow DOM стоит сказать пару слов. Он позволяет нам изолировать стили нативным способом, но:

  • Могут быть сложности с a11y

  • Сильно зависит от стека, и больше подойдет в web components based решении

  • Могут быть подводные камни с SSR. Хотя декларативный Shadow DOM уже неплохо поддерживается, но лично я с этим не работал, поэтому не могу сказать, какие здесь могут быть сложности

Atomic CSS

Мое впечатление от Atomic CSS
Мое впечатление от Atomic CSS

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

Если вдруг на этом этапе вы все еще не понимаете, что такое Atomic CSS, то поясню. Это такой подход, в котором мы для верстки используем маленькие атомарные классы (они еще называются утилитами), каждый из которых делает одно простое действие. Например, применяет одно CSS-свойство, но не обязательно одно.

Как выглядит Atomic CSS
Как выглядит Atomic CSS

Вы наверное думаете, что дальше я вам буду рассказывать, как "вот это вот" решает все вышеупомянутые проблемы верстки?

Ну давай, расскажи мне
Ну давай, расскажи мне

И будете правы!

Потому что:

  • Тратим меньше мыслетоплива. Не нужно думать про уникальные названия сущностей, БЭМ-блок это или БЭМ-элемент, какую делать структуру каталогов и etc

  • Меньше CSS на клиенте. С определенного момента разработки, стили перестают добавляться. Мы постоянно реиспользуем одни и те же утилиты

  • Быстрее пишем стили. Особенно, если используем короткие названия утилит. Плюс, нам сильно меньше нужно переключаться между файлами

  • Stack Agnostic. Подход можно использовать в любом стеке технологий мы можем использовать. Будь то у нас стандартные SPA на JS-фреймворке, вышеупомянутый Ruby или PHP, либо даже какая-то экзотика типа Elm или ClosureScript.

При этом, если взять последние опросы State of CSS, то там инструменты на основе атомарного подхода находятся в топе.

И вопреки распространенному мнению, Atomic CSS используется не только для лендингов и каких-то MVP, но также для серьезных SPA и крупных контентных проектов, что мы чуть позже более подробно рассмотрим.

Atomic CSS как решение

Теперь более подробно раскроем тезисы за Atomic CSS.

Экономия мыслетоплива

Рассмотрим небольшой пример. У нас есть секция со списком карточек погоды. И каждую карточку мы можем сделать, либо БЭМ-элементом, либо БЭМ-блоком. Тут уже возникает вопрос: что именно использовать?

Погода на день
Погода на день

Допустим, мы решили, что это БЭМ-элемент. Но как нам его назвать? Попробуйте придумать вариант

Название в оригинале

Тут все просто: forecast-briefly__day-link.

Хорошо, а вот похожий немного элемент в другом месте. Как бы вы его назвали?

Погода на час
Погода на час
Название в оригинале

Ну здесь тоже все более или менее понятно: fact__hour.

А вот если бы мы работали в атомарном подходе, то увидели бы, какие нам нужно здесь применить стили, накидали утилит, типа D-ib Mnw9u P0;9 Txa и пошли дальше, не думая об этом всем.

Еще один похожий элемент
Еще один похожий элемент

А вот еще один похожий элемент! Как бы вы его назвали?

Ладно, дальше не буду вас мучить, поскольку у автора оригинала тут тоже возникли сложности. Возможно поэтому он забил на БЭМ и взял какое-то другое решение, поскольку в классе там что-то вроде: sc-a9fb3bce-4.

Малый размер стилей

Ниже представлен график из доклада нашего коллеги, где он рассказывал, как они переписали проект со стандартного подхода на атомарный. На графике заметно, как снизился общий объем стилей.

Уменьшение размера стилей при переходе на Atomic CSS
Уменьшение размера стилей при переходе на Atomic CSS

Второй кейс от нашего коллеги Саймона: он точно так же взял библиотеку на основе Atomic CSS, переписал на ней свой проект и получил вот такие результаты:

Уменьшение размера стилей при переходе на Atomic CSS
Уменьшение размера стилей при переходе на Atomic CSS

Также можно вспомнить, пусть и старую, но неплохую, статистику от Yahoo, когда они сравнили размеры CSS на главных страницах популярных ресурсов, и у на их главной стилей оказалось меньше всего.

Статистика от Yahoo
Статистика от Yahoo

Напоследок можно привести более свежий кейс, где ребята переписали все со styled-components на Tailwind и получили хорошее улучшение некоторых метрик Core Web Vitals.

Улучшение метрики Core Web Vitals
Улучшение метрики Core Web Vitals

Скорость разработки

Ее померить уже сложнее, и релевантных данных об этом я не нашел. Даже если бы мы просто взяли какой-то сайт и попытались сверстать его в трех разных подходах, то после того, как мы бы сверстали его впервые, дальше было бы верстать повторно гораздо проще. Так что здесь я сошлюсь, во-первых, на личный опыт, а во-вторых - на цитаты некоторых авторов.

Например, наш коллега Саймон, который приводил статистику по снижению размера CSS, отметил в своей статье, что его продуктивность ощутимо выросла. В первую очередь, благодаря тому, что теперь не требуется переключать контекст с разметки на стили.

Your productivity is incredibly high because your brain doesn't need to context switch between markup and stylesheets.

© Simon Vrachliotis

А также второй наш коллега, который рассказывал про переписывание стилей со styled-components, отметил, что в их команде также наблюдался рост скорости разработки, и ребята остались довольны.

We have seen a significant improvement in our development speed and the feedback has generally been positive.

© Nikola Novak

Возможно вам понравились приведенные цифры и цитаты великих, но глядя на верстку в Atomic CSS вы могли подумать: "Что это за китайская грамота!? Сложнааа!". А вот давайте на этом моменте немного остановимся, попробуем слегка разобраться, и дальше вы сами сделаете вывод: сложно это или не очень.

Сложно
Сложно

Встречайте mlut

Логотип mlut
Логотип mlut

Все примеры в этом докладе сделаны с помощью mlut. Это такой инструмент для верстки в подходе Atomic CSS - моя разработка. И как вы могли заметить, в названиях утилит там используются сокращения. У них есть как плюсы, так и минусы

Плюсы

Минусы

Лаконичный код

Есть порог входа

Удобнее писать

Подойдут не всем

Чуть меньше вес кода

Лаконичный код, удобнее писать...
Лаконичный код, удобнее писать...

Я прекрасно понимаю что сокращения зайдут не всем, и это нормально. Ведь на рынке есть и другие Atomic CSS фреймворки. Стоит при этом пояснить, что сокращения взяты не с потолка, а составлены по единому алгоритму, который позволяет:

  • Избежать коллизий с новыми свойствами и значениями в CSS, поскольку он бурно развивается последние годы

  • Возможность выводить свойства "в голове", а не зазубривать их. Прокрутив так и использовав утилиту пару раз, вы быстро доведете ее до автоматизма, и больше не потребуется тратить мыслетопливо на ее вспоминание

Общий алгоритм сокращений

И сейчас мы немного с этим алгоритмом познакомимся. Для начала я приведу общий алгоритм. На практике вы его вряд ли будете использовать, поскольку он применяется, когда нам нужно составить сокращения для новых CSS свойств. Но для общего понимания, его все-таки стоит озвучить.

  1. Находим свойства, которые начинаются с одинаковой буквы

  2. Составляем их рейтинг по популярности (в основном, но не только)

  3. Выделяем группы с одинаковым пер��ым словом

  4. Составляем сокращения внутри групп

И при работе по такому алгоритму с CSS свойствами у нас получается вот такая табличка:

Таблица CSS-свойств и их сокращений
Таблица CSS-свойств и их сокращений

Здесь вы можете заметить, что свойства у нас отсортированы по алфавиту, разбиты на группы с одинаковым началом, где-то у них прописан рейтинг и какие-то сокращения.

Алгоритм сокращения одной сущности

А теперь будет алгоритм сокращения одной сущности, который как раз пригодится нам на практике.

I. Название сокращаем до первой буквы свойства/значения: color => C

II. Если название из нескольких слов, то берется первая буква из каждого слова: color-adjust => Ca

III. Если два названия имеют одну и ту же начальную букву, то в следующем названии, при сортировке их по рейтингу, добавляется буква

  1. color => C

  2. cursor => Cs

IV. Если название из N слов, то буква добавляется в соответствующем по порядку слове

  1. color => C

  2. cursor => Cs

  3. color-scheme => Csc

Порядок добавления буквы

Про порядок добавления буквы тоже стоит уточнить:

I. Согласная следующего слога: cursor => Cs

Если следующий слог начинается на гласную, то берется ближайшая предыдущая согласная от нее

II. Следующая согласная

  1. content => Сt

  2. contain => Cn

III. Следующая гласная (без перескока через согласную)

  1. content => Сt

  2. counter-increment => Coi

Тренировка

А теперь давай поупражняемся! Ниже будет несколько спойлеров: в title - сокращение, а внутри - свойство, которое ему соответствует. Попробуйте прокрутить их по алгоритму сокращения сущности, учитывая порядок добавления букв.

Ai

align-items, а не то, о чем вы подумали

Txt

text-transform

Ts

TypeScript transition

Fls

flex-shrink

Utility components syntax

Чтобы лучше понимать примеры кода, стоит еще разобраться в синтаксисе утилит. В mlut он называется Utility components syntax. Это такой синтаксис, в котором каждая утилита разбивается на компоненты, каждый из которых соответствует части CSS-правила. Под частями здесь подразумеваются at-rules, селектор, свойства и их значения.

Если мы внимательно посмотрим на вот такую утилиту:

  • какая-то ее часть соответствует медиа-выражению

  • какая-то - свойству и значению

  • и третья будет отвечать за модификатор селектора, например hover

А вот подробная схема этого синтаксиса:

  1. CSS at-rule: брейкпоинты, @supports, etc

  2. pre-states - часть селектора перед классом утилиты

  3. Имя

  4. Значение

  5. post-states - часть селектора после класса утилиты

Мифы про Atomic CSS

Теперь разберем некоторые мифы про Atomic CSS. Мы будем рассматривать только те мифы, о которых мало говорили в других материалах по теме.

Именно поэтому, сейчас мы не будем говорить, про один из самых популярных мифов. Миф о том, что Atomic CSS - это то же самое, что inline-стили. Короткий ответ - нет. А в качестве длинного ответа я вам могу порекомендовать доклад или статью Сары Даян, там она все подробно рассказала - можете ознакомиться.

"Это для тех, кто плохо знает CSS"

А вот про следующий миф мы уже поговорим. Стоит понимать, что Atomic CSS - это низкоуровневая штука. Здесь мы работаем практически на том же уровне, что и с обычным CSS - непосредственно со свойствами, с селекторами и вот этим всем.

Это значит, что, чем лучше мы знаем CSS:

  • тем лучше будет наш код

  • тем больше мы сможем сделать на чистом CSS, не прибегая к JS

Давайте рассмотрим пару примеров

Допустим, мы имеем какой-то декоративный элемент, у которого каждый угол разного скругления - вот дизайнер так нарисовал. И в атомарном подходе, если бы мы по-простому начали так писать, мы бы на каждый угол писали свою утилиту:

<div class="Bdtrr5u Bdtlr3u Bdbrr2u Bdblr8u">...</div>

Хотя тут можно было бы просто взять сокращенное свойство. Например, mlut такое позволяет:

<div class="Bdrd3u;5u;2u;8u">...</div>

Второй пример будет про интерактивные элементы, у которых есть состояния hover и focus. Предположим, дизайнер не нарисовал нам, что должно быть по фокусу. Но мы как грамотные разработчики интерфейсов знаем, что состояния нужно как-то выделять. В связи с этим, решили сделать самый простой вариант: чтобы у нас по focus применялись такие же стили, как по hover. Не очень хорошо зная CSS, в атомарном подходе мы бы начали дублировать стили для каждого состояния, и у нас бы получилось что-то подобное:

<div class="Bgc-red_h Bgc-red_fv">...</div> 

Но здесь можно было бы использовать множественные селекторы! Опять же, mlut такое умеет. Мы через запятую перечисляем псевдоклассы, на которых должна применится утилита. Соответственно, такая утилита превратится в следующий CSS.

Как видите, здесь есть множественный селектор и по :hover, и по :focus-visible.

Даже в каких-то сложных случаях, когда нам нужно, чтобы стили применялись по @supports, по какому-то медиа-выражению, современный атомарный CSS все это нам позволяет. Вот даже такие замудренные стили можно описать:

"Проблематично реиспользовать стили"

Следующий миф касается реиспользования стилей, особенно, вне компонентов. Кто-то может вспомнить такой функционал, как @apply. Но на самом деле, применять его для реиспользования - это скорее антипаттерн. Ведь в таком случае, все превращается в обычный ��укописный CSS, только написанный немного по-другому:

@layer components {
  .btn-primary {
    @apply py-2 px-4 bg-blue-400 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 
  }
}

Правильное же его назначение - для случаев, когда нам нужно в рукописных стилях использовать какие-то значения из утилит. Более же грамотное решение для реиспользования стилей - алиасы. Вот в чем заключается эта техника. Мы создаем глобальный словарь, ключи в котором - это алиасы, а значения - строки с повторяющимися наборами утилит.

module.exports = {
  dividerMain: "Bdb Bdw-div0 lg_Bdw-div1 Bdc-prml",
  h1Font: "Fz-h4 lg_Fz-h3 xxl_Fz-h2",
  link: "C-prm0 C-prm1_h Td Trs-btn",
  //...
}

Далее, когда нам в разметке нужно реиспользовать какие-то стили, мы достаем нужные утилиты из этого словаря по алиасу:

<a href="#" class="-Px3su <%= it.css.link %>">Back</a>

Внимательный зритель может здесь кое-что заподозрить...

Это же обычные CSS-правила!
Это же обычные CSS-правила!

Но вот в чем преимущества алиасов:

  • Существуют только в build-time - мы реиспользуем имеющиеся утилиты, без добавления нового кода

  • Алиасов сильно меньше, чем обычных CSS-классов - проблемы с неймингом вряд ли возникнут

  • Можно редактировать in-place через замену подстрок. Если в двадцати местах у кнопки стили одинаковые, а в двадцать первом поменялась жирность шрифта, то мы можем заменить утилиту как подстроку в нужном месте. Ведь алиасы - просто строки

"Слишком длинные className"

Еще один миф касается длины атрибутов class и раздувания XML с Atomic CSS. Давайте посмотрим на примере реального сайта, где используется БЭМ. И в одном из блоков вот такой длины получился атрибут class:

<!-- yandex.ru/pogoda -->

<td class="weather-table__body-cell weather-table__body-cell_type_daypart weather-table__body-cell_wrapper">...</td>

А когда я взял те же самые стили и переписал в атомарном подходе - получилось явно короче:

<td class="Pt40 Txa-l Txi0 Pb10 W94 Mnw-a Pl74 Va-m">...</td>

Но кто-то может подумать: "Ну это же Яндекс, там все у них сложно - поэтому такой серьезный БЭМ". А если взять другой популярный сайт, который вроде попроще, но там тоже используется БЭМ? Я провел такой же эксперимент, переписал в атомарном подходе стили, и эффект тот же самый:

<!-- habr.com -->

<!-- BEM -->
<div class="tm-article-comments-counter-link tm-data-icons__item">... </div>

<!-- Atomic CSS -->
<div class="Ct-n_af Ml32_+:& Ps D-f Ai-c H100p">...</div>

Здесь можно обратить внимание на утилиту Ml32_+:&, которая использует чуть более сложный селектор с комбинатором +. Она создает отступ между одинаковыми элементами. Конечно, это можно сделать сейчас по-разному, в частности, через gap. Но я по-честному скопировал стили из исходника - там было именно так. И даже в атомарном подходе это получилось записать достаточно коротко.

На всякий случай поясню, в какой CSS эта утилита превращается:

Также можно вспомнить статистику от Yahoo - они провернули что-то похожее: сравнили длину атрибуты class на разных сайтах. Результаты аналогичные.

Эксперимент Yahoo
Эксперимент Yahoo

Применение на практике

Теперь поговорим про best practices в атомарном подходе. Здесь несколько разделов:

  • применение custom properties

  • применение современного CSS

  • работа с контекстом

Custom properties

Первым делом разберем техники работы с custom properties:

  • прокидывание стилей в компонент

  • работа с цветами и темами

Прокидывание стилей

Когда начинаешь работать с атомарным CSS, один из вопросов, который возникает: "Как нам прокинуть стили в компонент, который находится на глубоком уровне вложенности?". Поскольку создавать на каждом уровне для этого пропсы - такое себе решение, то вот как мы можем поступить.

Допустим, у нас есть такое компонент для карточки:

// card.jsx

export function Card({item}) {
  return (
    <div className="P4u Bd Bgc-$cardBgc?#ccc ...">
      <img className="W-cardImgWidth?200 ..." alt="" src={item.img}/>
      <h3 className="...">{item.title}</h3>
      <a className="..." href="{item.link}">More</a>
    </div>
  );
}

В этом компоненте мы прописываем кастомные свойства в значениях утилит, соответствующих тем стилям, которые мы хотим поменять сверху. Можно это сделать даже с фолбэком, как в примере. На всякий случай поясню, в какой CSS превратятся такие утилиты:

И когда мы подключаем наш card.jsx, мы используем такую утилиту, которая устанавливает значение кастомного свойства:

// catalog.jsx

<section>
  <h2> Catalog </h2>
  <ul className="lg_-CardImgWidth350 ...">
    {item.map((item) => (
      <li key={item.id} className="...">
        <Card item{item}/>
      </li>
    ))}
  </ul>
</section>

Опять же поясню, в какой CSS оно превратится:

И таким же образом где-то в другом месте мы сможем поменять другие стили:

// recommended.js

<aside> 
  <h2>Recommended</h2>
  <ul className="-CardBgc-$blue200 ...">
    {items.map((items) => (
       // using our <Card>
    ))}
  </ul>
</aside>

Даже можно прокидывать сразу несколько стилей, если использовать сокращенные CSS-свойства. Так не придется для каждого стиля прописывать кастомное свойство в утилиту:

// card.jsx

<div className="P4u Bd-$cardBd?1;s ...">
  <img className="..." alt="" src={item.img}/>
  <h3 className="...">{item.title}</h3>
  <a className="..." href={item.link}>More</a>
</div>

Цвета и темы

В популярных Atomic CSS инструментах обычно используется такой подход, когда у нас цвета записываются в некоторые константы, а эти константы уже используются в значениях утилит. Для каких-то MVP или лендингов это может подойти. Но в каком-то минимально приличном продакшне нас завтра могут попросить сделать темную тему, и мы будем по всем компонентам бегать с горящим одним местом, пытаясь добавлять утилиты на для этой темы - не самый грамотный подход.

// card.jsx

<div className="Bgc-slate50 :-dark_Bgc-slate800 C-slate700 :-dark_C-slate300">
  <img className="..." alt="" src={item.img}/>
  <h3 className="...">😢</h3>
  <a className="..." href={item.link}>More</a>
</div>

Более правильное решение - использовать те же самые кастомные свойства для цветов. То есть, мы наши цвета записываем в кастомные свойства, а затем их используем в значениях утилит.

// card.jsx

<div className="Bgc-$gray800 C-$secondary200 ...">
  <img className="..." alt="" src={item.img}/>
  <h3 className="...">{item.title}</h3>
  <a className="..." href={item.link}>More</a>
</div>

И в зависимости от наших задач, мы можем переключать цвета у кастомных свойств тем или иным способом. Вот здесь я привел самый простой способ: на уровне CSS просто при темной теме мы меняем значения цветов в кастомных свойствах:

:root {
 --ml-gray800: #e6e8e8;
 --ml-secondary200: #f0438c;
 /*...*/

 @media (prefers-color-scheme: dark) {
   --ml-gray800: #27272c;
   --ml-secondary200: deeppink;
   /*...*/
 }
}

Современный CSS

Сейчас в CSS много чего добавляется, поэтому здесь я выделил две важные фичи. На мой взгляд, они наиболее сильно повлияли на то, как мы верстаем:

Container queries

Здесь будет стандартный пример: у нас есть две карточки - обычный грид на две колонки.

Вот так мы его описываем в разметке:

<section>
  <h2>Catalog</h2>
  <ul className="D-g Gtc-t2 Gap6u ...">
    {items.map((item) => (
      <li key={item.id} className="...">
        <Card item={item}/>
      </li>
    ))}
  </ul>
</section>

Но в какой-то момент у нас добавляется сайдбар. Допустим, он у нас может выезжать по некоторому действию. Соответственно, нам не подходят медиа-выражения, чтобы на брейкпойнте карточки как-то свернулись в одну колонку. Так что с развернутым сайдбаром все выглядит не очень.

Тут нам и помогут container queries. Прописываем container-type нашему компоненту и как раз в списке добавляем выражение от контейнера @c:w>=520_Gtc-t2. Теперь только при определенном размере контейнера, грид будет в две колонки.

Опять же к вопросу о важности знания CSS. Зная, что грид по умолчанию идет в одну колонку, нам не нужно писать несколько утилит - мы просто к существующей утилите, задающей две колонки, дописали выражение от контейнера.

<section className="Ctnt-is">
  <h2>Catalog</h2>
  <ul className="D-g @c:w>=520_Gtc-t2 ...">
    {items.map((item) => (
      <li key={item.id} className="...">
        <Card item={item}/>
      </li>
    ))}
  </ul>
  <aside className="...">
    Sidebar
  </aside>
</section>

И вот мы добились нужного поведения.

На всякий случай, опять же поясню, как такая утилита развернется в CSS:

Понимаю, что выглядит это пока непривычно, но согласитесь, работает оно достаточно мощно.

Ну нафиг!
Ну нафиг!

Псевдокласс :has()

На всякий случай напомню, как он работает. Правило с :has() будет применено, если хотя бы один из селекторов, переданных аргументом в :has(), выберет хотя бы один элемент.

Рассмотрим следующий пример. Допустим, у нас есть форма с некоторыми полями.

Разметка у нее стандартная, когда у в <label> содержится <input> и соответствующая ему подпись:

<form class="Bd P4u">
  <label class="...">
    <!-- ... -->
  </label>
  <label class="D P2u ...">
    <span class="...">Email</span>
    <input class="..." name="email" type="email"/>
  </label>
</form>

И мы хотим, чтобы весь наш <label> подсветился красным аутлайном, если в поле ввели какое-то невалидное значение.

Чтобы этого добиться, мы добавляем утилиту с has, которая применит стили, только если где-то внутри сработал user-invalid.

<form class="Bd P4u">
  <label class="...">
    <!-- ... -->
  </label>
  <label class="D P2u Ol3;red;dh_has(ui)...">
    <span class="...">Email</span>
    <input class="..." name="email" type="email"/>
  </label>
</form>

В такой CSS она развернется:

Работа с контекстом

Контекст - это одна из уникальных фич в mlut. В других Atomic CSS инструментах подобное тоже можно сделать, но там это либо сложнее, либо как-то костыльно.

Контекст - это специальная утилита, которая не применяет стили, но с которой удобно комбинировать другие утилиты в сложных селекторах. В синтаксисе обозначается "крышечкой" ^. Выглядит это вот так:

Целевая утилита сработает только в том случае, если где-то сверху будет утилита контекста -Ctx с псевдоклассом.

Контекст:

  • можно использовать с любыми утилитами

  • можно как угодно комбинировать в states

  • может быть именованным

Например здесь мы задали имя one, чтобы использовать несколько контекстов рядом друг с другом и чтобы они не конфликтовали.

Есть следующие техники работы с контекстом

  • Group state: hover/focus/etc

  • Стилизация элемента компонента

  • CSS-only интерактивность

Group state

Пусть у нас есть небольшая карточка, завернутая в ссылку.

<a href="#" class="D C-white ...">
  <h3 class="Mt0">Some card</h3>
  <p>...</p>
  <span href="#" class="D-ib P2u;5u ...">More</span>
</a>

И мы хотим, чтобы по :hover она как-то поменяла стили, а кнопка стала красной.

Вот как этого добиться в атомарном подходе. На карточку ставим утилиту контекста, а на нашу кнопку помещаем утилиту для изменения цвета фона, чтобы он становился красным, когда на карточке сработал псевдокласс :hover.

<a href="#" class="D C-white -Ctx ...">
  <h3 class="Mt0">Some card</h3>
  <p>...</p>
  <span href="#" class="D-ib P2u;5u ^:h_Bgc-red ...">More</span>
</a>

Стилизация элемента компонента

Дальше мы хотим, чтобы вышеупомянутая карточка в каком-то другом окружении поменяла как-то свои стили. Например, как здесь: у кнопки текст стал uppercase и убрались закругления.

Такую стилизацию можно получить разными способами, но мы здесь рассмотрим именно вариант с контекстом. В нашей карточке мы ставим именованную утилиту контекста уже на кнопку.

// miniCard.jsx

<a href={item.link} className="D C-white ...">
  <h3 className="Mt0">{item.title}</h3>
  <p>{item.text}</p>
  <span href="#" class="D-ib P2u;5u -Ctx-btn ...">More</span>
</a>

И выше, там где мы ее подключаем, можно добавить утилиты, которые применятся к элементу с именованным контекстом.

<div className="... Bdrc0_:^btn Txt_:^btn">
  <h2>Wiki</h2>
  <MiniCard item={mainItem}/>
</div>

CSS-only интерактивность

Вспомним форму, с которой работали еще недавно. Теперь мы хотим, чтобы у нашего невалидного поля появлялась подпись, что оно является таковым.

Здесь мы реализуем простую ui-логику прямо на CSS. На <input> помещаем утилиту контекста, а дальше на подпись ставим утилиту, которая применится только в том случае, если перед ней утилита контекста получила псевдокласс :user-invalid

<form class="Bd P4u">
  ...
  <label class="D P2u ...">
    <span class="...">Email</span>
    <input class="... -Ctx" name="email" type="email"/>
    <span class="D-n C-red ^:ui:+_D ...">Invalid email</span>
  </label>
  ...
</form>

Кейсы

Напоследок рассмотрим несколько крупных проектов на Atomic CSS.

The Verge

Скриншот главной сайта
Скриншот главной сайта

Крупный новостной сайт про технологии с посещаемость в месяц больше 50 миллионов. Используется Tailwind и стандартный React-стек.

Как можно заметить, там нестандартный дизайн, адаптивность и тд. В атомарный подход все прекрасно укладывается.

Lemon Squeezy

Скриншот дашборда
Скриншот дашборда

Платформа для продажи цифровых товаров онлайн. Там есть не только сайт с контентом, но и полноценное SPA. Работает на Tailwind, Vue и Laravel.

Yahoo

Скриншот главной страницы
Скриншот главной страницы

Ну и стоит упомянуть старый добрый Yahoo. У них небольшая экосистема: новости, финансы, почта и тд. Некоторые продукты также использует Atomic CSS (Atomizer). Можно зайти на главную страницу и посмотреть, как все это сделано.

Заключение

Подводя итоги, можно сказать, что Atomic CSS хорошо подойдет

  • для лендингов - Netflix Global Top 10

  • для объемных сервисов с большим количеством контента - Der Spiegel

  • средних и относительно крупных продуктов - Tinder

При этом он вряд ли подойдет:

  • для проектов с Rich UI, типа Google Docs и Figma

  • для огромных продуктов и экосистем, типа Яндекса

По крайней мере, я пока не пробовал верстать второй Яндекс в таком подходе, поэтому сомневаюсь, что оно здесь подойдет.

Но если вдруг у вас именно такие проекты, то стоит сказать о достойных альтернативах

  • Если много динамики, то где-то может подойти классический CSS-in-JS

  • Если все совсем сложно, то тут уже Canvas, WASM, а также $mol хорошо зайдет

  • Если же речь про огромный продукт или экосистему, то $mol будет лучшим вариантом

Последний пункт стоит немного пояснить. В $mol используются типизированные стили - что-то похожее на CSS-in-JS только в TS. Если вы в CSS-свойстве написали какое-то невалидное значение, вам TypeScript выдаст ошибку. И даже если на несуществующий компонент попытаетесь какие-то стили написать, то точно так же у вас компиляция TS не пройдет. Плюс ко всему, там есть и другие значимые преимущества: автоматическая виртуализация рендеринга, малый размер бандла и тд. Но думаю, активные читатели Хабра об этом уже наслышаны.

Но возвращаясь к атомарному CSS, надо понимать, что это инвестиционная история. Да, в начале у вас есть какой-то порог вхождения, вам надо разобраться, приложить усилия. Но вложившись один раз в начале, дальше вы будете получать профит всю оставшуюся практику.

Лучше день потерять, потом за 5 минут долететь.

Какие выводы теперь можно сделать про Atomic CSS:

  • Это не один фреймворк

  • Это ни в коем случае не замена CSS

  • Это полноценная методология со своими плюсами и минусами

Соответственно, если немного разобраться и использовать хорошие практики подхода, то вы легко сможете обойти его слабые места и получить заявленный эффект. Ну и конечно, почувствовать ту самую легкость бытия в процессе верстки.

На этом все! Подписывайтесь на мой телеграм-канал, ставьте звезды на гитхабе, ну и буду рад видеть ваши комменты!

А еще, мы недавно выкатили первую версию онлайн-песочницы - теперь mlut можно попробовать прямо в браузере! Сейчас это скорее mvp, но будем развивать ее и дальше.

P.S. особенно жду шутки про эльфийский язык и использование mlut, как генератора паролей!

P.P.S концовка доклада традиционная, как вы любите.