Здравствуйте, товарищи!
Меня зовут Валентин, и сегодня мы снова поговорим про 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 году я достаточно много верстал и конкретно угорал по БЭМ-у. К маю того года я пересмотрел, наверное, все доклады и статьи на эту тему.

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

Но сейчас уже далеко не 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, то поясню. Это такой подход, в котором мы для верстки используем маленькие атомарные классы (они еще называются утилитами), каждый из которых делает одно простое действие. Например, применяет одно 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, переписал на ней свой проект и получил вот такие результаты:

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

Напоследок можно привести более свежий кейс, где ребята переписали все со styled-components на Tailwind и получили хорошее улучшение некоторых метрик 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. Это такой инструмент для верстки в подходе Atomic CSS - моя разработка. И как вы могли заметить, в названиях утилит там используются сокращения. У них есть как плюсы, так и минусы
Плюсы | Минусы |
|---|---|
Лаконичный код | Есть порог входа |
Удобнее писать | Подойдут не всем |
Чуть меньше вес кода |

Я прекрасно понимаю что сокращения зайдут не всем, и это нормально. Ведь на рынке есть и другие Atomic CSS фреймворки. Стоит при этом пояснить, что сокращения взяты не с потолка, а составлены по единому алгоритму, который позволяет:
Избежать коллизий с новыми свойствами и значениями в CSS, поскольку он бурно развивается последние годы
Возможность выводить свойства "в голове", а не зазубривать их. Прокрутив так и использовав утилиту пару раз, вы быстро доведете ее до автоматизма, и больше не потребуется тратить мыслетопливо на ее вспоминание
Общий алгоритм сокращений
И сейчас мы немного с этим алгоритмом познакомимся. Для начала я приведу общий алгоритм. На практике вы его вряд ли будете использовать, поскольку он применяется, когда нам нужно составить сокращения для новых CSS свойств. Но для общего понимания, его все-таки стоит озвучить.
Находим свойства, которые начинаются с одинаковой буквы
Составляем их рейтинг по популярности (в основном, но не только)
Выделяем группы с одинаковым пер��ым словом
Составляем сокращения внутри групп
И при работе по такому алгоритму с CSS свойствами у нас получается вот такая табличка:

Здесь вы можете заметить, что свойства у нас отсортированы по алфавиту, разбиты на группы с одинаковым началом, где-то у них прописан рейтинг и какие-то сокращения.
Алгоритм сокращения одной сущности
А теперь будет алгоритм сокращения одной сущности, который как раз пригодится нам на практике.
I. Название сокращаем до первой буквы свойства/значения: color => C
II. Если название из нескольких слов, то берется первая буква из каждого слова: color-adjust => Ca
III. Если два названия имеют одну и ту же начальную букву, то в следующем названии, при сортировке их по рейтингу, добавляется буква
color=>Ccursor=>Cs
IV. Если название из N слов, то буква добавляется в соответствующем по порядку слове
color=>Ccursor=>Cscolor-scheme=>Csc
Порядок добавления буквы
Про порядок добавления буквы тоже стоит уточнить:
I. Согласная следующего слога: cursor => Cs
Если следующий слог начинается на гласную, то берется ближайшая предыдущая согласная от нее
II. Следующая согласная
content=>Сtcontain=>Cn
III. Следующая гласная (без перескока через согласную)
content=>Сtcounter-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
А вот подробная схема этого синтаксиса:

CSS at-rule: брейкпоинты,
@supports, etcpre-states - часть селектора перед классом утилиты
Имя
Значение
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>
Внимательный зритель может здесь кое-что заподозрить...

Но вот в чем преимущества алиасов:
Существуют только в 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 на разных сайтах. Результаты аналогичные.

Применение на практике
Теперь поговорим про 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 много чего добавляется, поэтому здесь я выделил две важные фичи. На мой взгляд, они наиболее сильно повлияли на то, как мы верстаем:
псевдокласс :has()
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 концовка доклада традиционная, как вы любите.
