Всем привет! Меня зовут Андрей, я профессионально разрабатываю веб-интерфейсы уже больше 11 лет и последний год развиваю проект Numl, который можно назвать языком разметки и стилизации для веб. В этой статье я расскажу, как в попытке перебороть ряд особенностей CSS и упростить вёрстку веб-проектов получился целый язык, который не только удовлетворил все наши потребности в стилизации, но также позволил уменьшить кол-во JS-кода и улучшить доступность.  Для начала, коротко про Numl и чем он может быть интересен разработчикам. Numl это язык разметки, который объединяет в себе функции CSS-фреймворка, JS-фреймворка без композиции и Дизайн-системы, и предоставляет набор готовых элементов, каждый из которых имеет обширный набор свойств для кастомизации. Язык основывается на нативном браузерном API Custom Elements из спецификации Web Components, и совместим с популярными JS-фреймворками, такими как Vue, Svelte, Angular и React. Отличительной (и я бы даже сказал "уникальной") чертой Numl является то, что все стили для интерфейса он генерирует в runtime, что позволяет выжать максимум из CSS и добиться огромной гибкости в стилизации и кастомизации элементов. Эта статья — ответ на вопрос, как так получилось и почему такой подход заслуживает право на жизнь. На прошлой неделе, 4-го июля, проекту исполнился ровно год и он уже давно прошёл стадию proof of concept. На нём написан крупный проект Sellerscale и браузерное расширение от Sellerscale. Также с помощью Numl создано еще несколько сайтов, включая собственный лэндинг и Storybook. Полный набор ссылок будет в конце. 
Дашборд Sellerscale. Стэк: VueJS, Numl 
Расширение Sellerscale. Стэк: Svelte, Numl Numl сам по себе может быть интересен всем, кто знаком с основами HTML/CSS и хочет создавать качественные, доступные и красивые веб-интерфейсы, без глубокого погружения в тонкости CSS и ARIA. Однако данная статья выходит за рамки базовых знаний и больше подойдёт для людей, которые разбираются в различных CSS методологиях, много верстают, пишут свои инструменты для стилизации или же интересуются необычными инструментами из мира фронтенд-разработки. Используете Utility-First CSS? Тогда вам определённо стоит дочитать до конца. Для тех, кому просто хочется узнать про Numl, я предлагаю посетить сайт numl.design, полистать Storybook с кучей примеров, почитать статью про базовый синтаксис в гайде и попробовать Numl прямо в браузере с помощью REPL. Программировать я начал примерно 22 года назад, и уже тогда, используя Turbo Pascal, создавал различные оконные интерфейсы с кнопочками, окнами, инпутами и прочим. Двумя годами позднее, изучив веб-платформу я принялся за разработку сайтов и с тех пор моё хобби стало плавно превращаться в профессию. Многие веб-разработчики овладев HTML/CSS, погружаются в JS-экосистему и постепенно перестают верстать. Но мне всегда хотелось создавать качественные интерфейсы, а это невозможно без вёрстки как активного навыка. Поэтому, если было время, я старался верстать проекты самостоятельно, применять новые методологии, новые CSS-свойства, новые хаки.  В основном я использовал методологию, очень похожую на БЭМ, только без Modifier Value (впрочем почти все его так и используют). Это не было идеальным, но позволяло верстать качественно и с относительно большой скоростью, так как стили имели чёткую структуру. Можно было располагать куски кода в нужном месте, не сильно задумываясь. Спустя много лет, пришло понимание, что независимо от методологии, у CSS есть ряд особенностей, которые очень сложно упростить или скрыть за абстракцию. Далее я попробую их перечислить. Хочу сразу отметить, что я питаю огромное уважение к разработчикам веб-стандартов, ценю их огромный труд, и не пытаюсь его обесценить. Веб-стандарты в первую очередь дают нам больше возможностей для решения наших задач, но это не означает, что эти стандарты обязаны быть идеальными для каждого отдельно взятого разработчика, проекта, компании или отдельной задачи. Поэтому это не список "проблем CSS", а список его особенностей в контексте создания больших и сложных интерфейсов: Причём создание обёрток посредством препроцессоров может даже усложнять ситуацию, добавляя в и без того сложную абстракцию дополнительное измерение. А бывает и так, что попытка что-то упростить начинает сильно нас ограничивать. Например, фиксированные breakpoints для адаптивности почти в каждом первом CSS-фреймворке. Я считаю, что в хорошем инструменте простые задачи должны решаться просто, а сложные — пропорционально сложнее. И CSS прекрасно вписывается в эту концепцию, пока мы создаём относительно простые сайты с небольшим кол-вом требований, но по мере усложнения задач и объёмов проектов, поддержка CSS становится несоизмеримо более затратной. Всё это привело меня к идее, что нужно искать новые подходы к стилизации. Разумеется, я не надеялся побороть все описанные выше особенности CSS, но мне хотелось разобраться хотя бы с некоторыми из них. Начиная один новый крупный проект, я решил поставить эксперимент и не использовать CSS вне компонентов. Это позволило бы минимизировать технический долг в будущем и мне было интересно посмотреть куда меня это приведёт, ведь подобный подход не сильно популярен, а значит в этом направлении возможно мало копали. Самой первой проблемой стала раскладка страниц. Да, разумеется, можно создать компоненты MyGrid или MyFlex с соответствующим display свойством, что я и сделал, но для задания item-свойств (basis/width/height) нужно было что-то придумать и я решил эту проблему создав Базовый элемент (далее просто БЭ) от которого все остальные компоненты должны были наследоваться. Таким образом каждый компонент получил свойства basis, width, height и другие, что позволило корректировать их размеры и создавать адекватную раскладку. Также с самого начала были добавлены свойства size и text, которые относились к тексту. size использовался для выставления font-size/line-heigh, а текст накладывал различные модификаторы текста (атомарный css в чистом виде), чтобы можно было легко делать текст жирным, выставлять выравнивание и т.п. Всё это работало на обычных inline-стилях. На данном этапе проекта, адаптивность под мобильные не требовалась и я решил, что такой подход имеет место быть, учитывая, что он позволял нам очень быстро итерироваться по различным версиям дизайна, не жертвуя качеством и не создавая технический долг. После трех месяцев оказалось, что данный подход не только улучшает качество кодовой базы и ускоряет разработку, но и улучшает DX (Developer Experience). Больше не требовалось переключение между контекстами разметка/стили для вёрстки страниц и составных компонентов.  Кол-во свойств БЭ росло, всё больше помогая быстро решать локальные задачи вёрстки. Но появились и первые проблемы. Всю сложную кастомизацию элементов приходилось уносить в свойства и некоторые компоненты сильно от этого пухли. Качество кода самих компонентов существенно снижалось. Чем больше элементов имел внутри себя компонент, тем больше проблем вызывала его кастомизация. Чтобы это исправить, я решил по максимуму отказаться от компонентов в пользу обычных элементов. Вот типичный элемент изнутри: Ура, первый кусочек кода! Т.е. никакой композиции других элементов внутри нет. Композицией мы занимаемся на верхнем уровне. Пример для наглядности: Мы объявили кнопку с попапом внутри, создав сложный интерактивный компонент. Теперь мы можем достаточно гибко кастомизировать всё что находится и внутри самой кнопки, и внутри попапа без дополнительных свойств. Там где гибкости не требуется мы можем завернуть это всё в отдельный компонент MyDropdown, чтобы не писать лишний код. Спустя месяц мне настолько понравилось использовать этот подход, что я просто выкинул большую часть компонентов, заменив их элементами. В этот момент стало очевидно, что подход очень удобен и надо думать, как его развивать, чтобы добавить адаптивность, контекстные стили, стили для состояний, более удобную работу с цветом (у нас в проекте было много раскрашенных элементов). Примерно в это время я давал мастер-класс по гридам и для упрощения демок создал простенький веб-компонент my-grid, который брал атрибуты и мапил их на стили. Я был поражен насколько хорошо мой подход вписался в концепцию Custom Elements. Ведь, в них по умолчанию нет никакой композиции! Следующие несколько дней я потратил на миграцию элементов нашего проекта на Custom Elements, а сами элементы вынес в отдельный Open Source проект NUDE Elements, название которого позже было сокращено до Numl. Все элементы получили префикс nu-.  Миграция прошла на удивление легко. И началась активная работа над Numl параллельно основному проекту. В первую очередь нужно было избавиться от inline-стилей. Это сильно ограничивало возможности CSS и потребляло лишнюю мощность на маппингах. Таким образом был создан механизм генерации CSS в рантайме. Если упрощенно, то работало это следующим образом: Элемент nu-grid получал в атрибут columns значение 1fr 1fr. Генератор анализирует это, вызывает функцию columnsAttr('1fr 1fr'), которая выглядит следующим образом: Дальше генератор берёт результат и создаёт на его основе CSS: … и вставляет это всё в <style>, который в свою очередь вставляется в <head>. Разумеется алгоритм проверял, есть ли уже идентичный кусок CSS, чтобы не создавать дубликат. Ожидаемого ускорения сайта это не принесло (всё и до этого работало мгновенно), но и медленнее не стало, а мы получили отличный механизм, который позволил пользоваться всеми благами CSS. Может показаться, что это очень ресурсоёмко, но такой атомарный подход позволяет существенно сэкономить на кол-ве самого CSS (спросите адептов Atomic CSS), а некоторые адепты CSS-in-JS могут с удовольствием вам рассказать, что динамическая генерация CSS не такой уж сильный оверхед. Получив в свой арсенал полноценный CSS, я стал добавлять всё новые и новые свойства, по возможности упрощая их синтаксис.  Например, Numl обзавёлся дефолтными значениями для свойств. Это оказалось довольно удобно, ведь за простым <nu-block border> может скрываться достаточно выразительная вещь вроде: , где мы выставляем бордер с толщиной и цветом по умолчанию. Извечная проблема позиционирования стала решаться одним свойством place. Например, такое значение прилепит элемент снаружи карточки по середине верхней грани. Открыть в REPL То же свойств можно использовать для позиционирования внутри grid/flex-раскладки, для float-позиционирования, fixed и sticky. В CSS эти свойства всё равно взаимоисключающие. Иные свойства наоборот разделились. Например, transform стало возможно использовать оперируя разными свойствами: Открыть в REPL Также, для удобства я добавил кастомные юниты в синтаксис, что позволило упростить написание стилей: Открыть в REPL Где x — базовый gap. Кстати о gap'ах. Мне очень нехватало его для Flexbox, и я решил его добавить: Открыть в REPL Подробнее можно посмотреть в Storybook.  Идей было очень много. Возможность создавать сложные стили и прятать их за простой API манила и было уже очень сложно остановиться. В какой-то момент возможности стилей Numl стали настолько продвинутыми, что стилизация всех элементов была на них переписана. Однако, стилями возможности Numl не ограничились. То, что у меня получилось, вполне можно назвать атомарным подходом. Однако, у такого подхода есть один большой недостаток — отсутствие возможности стилизовать несколько элементов единой декларацией. В CSS мы делаем это с помощью классов и это кажется настолько естественным и привычным, что отказ от классов может показаться безумием. Однако, после 4 месяцев работы с подобным подходом, оказалось, что классы слегка переоценены, ведь за редким исключением, я не получил никакого дискомфорта от атомарного подхода. В композиционных фреймворках есть два отличных решения для такой задачи: циклы и выделение в компонент.  И всё же иногда цикл не применим, а создание отдельного компонента кажется излишним усложнением. Например, в лэндингах, где много уникальных элементов с общими характеристиками, вроде отступов и размеров. В этом случае атомарный подход показывает себя не лучшим образом.  Решить эту проблемы было отличным вызовом для Numl. Так появился первый элемент-объявление <nu-attrs>, который задаёт нужные атрибуты для элементов с определённым именем в контексте элемента-родителя. Причем атрибуты могут быть не только те, что используются для стилизации. Вот пример, как это работает: Открыть в REPL После такого объявления, каждая кнопка получила атрибут color="special". Более того, если вы динамически начнёте менять (или даже удалять!) атрибуты у <nu-attrs>, то же самое начнёт происходить и с кнопками. Атрибуты различных <nu-attrs> применяются каскадом, поэтому вы можете применить объявление даже к корневому элементу, это не помешает работе других объявлений уровнем ниже. В случае коллизии, значение атрибута из более близкого по иерархии объявления будет приоритетнее. Данный механизм получился в каком-то плане даже лучше, чем классы, ведь наложение классов может приводить к коллизии стилей, а здесь она исключена. Разница становится еще более очевидной, когда мы начинаем привязывать стили к состояниям (об этом чуть ниже). Узнать больше про <nu-attrs>и посмотреть примеры можно вот тут. Я достаточно давно мечтал реализовать механизм для раскрашивания сайтов. Идея была в том, чтобы на основании минимального кол-ва информации генерить полноценные темы, с тёмным вариантом, чтобы пользователям было комфортно пользоваться интерфейсом по вечерам и при слабом освещении. Идеально было бы передавать некий оттенок (число от 0 до 359 в HSL) и получать набор Custom Properties для раскрашивания всего сайта, в котором цвета уже имеют необходимую насыщенность и контрастность.  Также хотелось решить задачу сочетания цветов. Когда у нас несколько цветных элементов (например, цветные заголовки страниц или список тегов), то хочется, чтобы их яркость и насыщенность была выровнена для лучшей визуальной гармонии. 
Модуль юнит-экономики. Sellerscale Звучит интересно, но по факту это несёт в себе уйму технических и дизайнерских вызовов: Эта фича заняла у меня три с лишним месяца, было сделано шесть крупных итераций. В конечном итоге получилась очень удобная система, в которую удалось добавить даже Режим высокой контрастности. Как это работает в простом виде можно посмотреть на моей домашней страничке: tenphi.me (кнопочки настройки темы наверху) или в меню настроек в Storybook. Также есть интерактивная демка с градиентами на CodePen написанная чисто на Numl. 
Раздел с демонстрацией механизма тем на лэндинге numl.design 
То же самое в тёмном варианте 
Окно настроек темы в Storybook Разумеется, можно использовать и отдельные цвета, для этого была создана кастомная функция hue(), с помощью, которой можно генерить адаптивные цвета, задавая их в выдуманном специально для Numl переменном цветовом пространстве HSC (Оттенок, Насыщенность, Контрастность). Градиент на моей домашней страничке сделан как раз с помощью этой функции. 
Моя домашняя страничка. Демонстрация цветовых возможностей Numl. tenphi.me Благодаря темизации Numl превратился в довольно продвинутую Дизайн-систему, которую легко кастомизировать под себя. Я планирую написать отдельную большую статью про алгоритм темизации, и поделиться решением, которым можно было бы использовать отдельно от Numl. А сейчас возвращаемся к нашим элементам... Следующим вызовом было добавление мобильной версии для нашего сайта. К счастью, к тому моменту у меня в голове уже была готовая спецификация для этого. Выглядело (и выглядит до сих пор) это примерно так: мы не используем глобальные breakpoint'ы, вместо этого мы их объявляем в контексте конкретного элемента, разделяя символом | (вертикальная черта), причем кол-во точек не ограничено. Это позволяет нам менять breakpoint'ы на любом уровне, а его дочерние элементы эти точки наследуют: Две точки адаптивности создают три зоны значений для каждого свойства (также, как две точки на отрезке делят его на три части). Теперь нужно эти значения как-то различать в свойствах. К счастью, для этого есть всё тот же символ |: Открыть в REPL Так мы определили значение для каждой зоны. Если значение при переходе точки не меняется, просто пропускаем его. Чуть подробнее можно почитать тут. После создания данной системы, добавление адаптивности для нашего проекта заняло всего один день. Результат поистине впечатляющий, если учесть, что мобильная версия изначально не закладывалась в дизайн и над некоторыми элементами пришлось попотеть. 
Скриншоты мобильной версии Sellerscale Разумеется, чтобы система была достаточно гибкой, в ней должна присутствовать возможность привязывать стили к различным состояниям объекта. Также, как мы знаем, состояния могут быть и кастомными (вспомните модификаторы из БЭМ), поэтому всё должно быть максимально гибко.  Я остановился на следующем синтаксисе: Открыть в REPL Тут мы говорим, что у элемента нет тени в базовом состоянии, но она появляется, когда мы наводим курсор. Можно привязать стиль и к родителю, например: Открыть в REPL Есть и другие чуть более сложные варианты синтаксиса. Прелесть здесь в том, что внутренний алгоритм создаёт взаимоисключающие селекторы для каждого стиля: Не так впечатляюще выглядит при двух значениях. А что если их три или четыре? Numl найдёт селектор для любой их комбинации и вставит значение. Если значение не описано, то он выставит для него базовое значение (в примере выше это text). Этот алгоритм позволяет использовать различные значения CSS свойств для одного стиля, не боясь, что они перемешаются! Ну и браузер, уверен, будет благодарен, что ему не нужно разбираться с коллизией стилей. Создать кастомный модификатор можно с помощью атрибута начинающегося на is-. Синтаксис использования от этого не меняется: Открыть в REPL Добравшись до этой точки, я заметил, что все перечисленные вначале особенности CSS больше не являются проблемой для моего приложения: Звучит невероятно, но я дважды проверял, всё действительно так. :) Однако, если уж мы решили создать свой собственный полноценный язык разметки, то нам рано останавливаться. Тут никак не обойтись без еще двух очень важных составляющих: Поведения и инструменты доступности. Мы абстрагировали сложные стили за простой абстракцией, но что если сделать то же самое для базовых поведений? Например, в HTML, когда мы вставляем обычную кнопку <button> она уже из коробки умеет нажиматься и держать фокус. Чтобы сделать аналогичный механизм в Numl, ничего придумывать не пришлось. Есть достаточно широко применяемые директивы во фреймворках. В Numl была реализована их альтернатива под названием Behaviors. Работает это просто. Берём нужное поведение и инжектируем его с помощью атрибута, добавляя префикс nx- к его названию. Открыть в REPL — пример кнопки на базе nu-el. Разумеется, для элемента nu-btn это поведение будет добавлено по умолчанию вместе базовыми стилями. Всего в Numl таких поведений больше 35-ти. Все они загружаются асинхронно по мере необходимости. Есть крохотные поведения, а есть очень комплексные, например поведение для валидации форм, подсветка синтаксиса и даже конвертор из Markdown в Numl! 
Numl Storybook: пример конвертации Markdown->Numl Существует много статей и докладов про необходимость доступности в современном вебе. Однако, не так много инструментов, которые бы упрощали создание доступных интерфейсов, а не просто сыпали ворнингами. (хотя безусловно это тоже очень полезно и важно!) И если создание доступных компонентов само по себе не является чем-то крайне сложным, то создание фронтенд-архитектуры заточенной под доступные интерфейсы с целью масштабирования может вызвать затруднения даже у самых опытных разработчиков. Поэтому разработка Numl почти с самого начала велась с оглядкой на спецификацию ARIA, чтобы максимально ей соответствовать и упрощать разработчику её использование. Сложности начинаются еще на этапе обыкновенной линковки. Спецификация требует привязывать элементы друг к другу используя уникальный идентификатор. Если мы создаём универсальный компонент, нам приходится заниматься генерацией ID, чтобы избежать коллизии. Но даже в обычной вёрстке это может причинять головную боль, заставляя писать ID вида button-23. Numl имеет собственный механизм идентификаторов. Выставляя ID на элементе, вы задаёте ему базовое значение идентификатора. Если оно не уникально, то оно заменится на уникальное с добавлением индекса. Однако, если вам потребуется сослаться на элемент по ID, вы можете использовать базовый ID, просто Numl найдёт ближайший элемент с таким базовым ID. Скорее всего, это именно то, что вы ожидаете. Открыть в REPL В отличие от обычного HTML, мы можем размножить данный кусок кода, не ломая доступность. В Numl большинство aria-атрибутов не используются напрямую. Чтобы, например, добавить описание для элемента, надо использовать сокращённую форму такого атрибута (без aria-), как в примере выше. Сокращенные атрибуты удобны и в других ситуациях: Опережу ваш вопрос. Нет, такой подход практически не вызывает коллизий. Помимо этого, Numl по умолчанию старается соответствовать ARIA спецификации, там где это возможно сделать без явного действия со стороны разработчика. Numl имеет огромное кол-во возможностей. Очень много задач, которые требовали от нас использования хитрых CSS или JS хаков теперь решаются "закулисно", мы можем о них даже не задумываться. Однако, такой подход имеет и особенности, которые могут устроить не всех: С другой стороны, Numl имеет очень сильные стороны, которые могут заинтересовать как начинающих разработчиков, так и опытных гуру: Хороший инструмент должен уважать ваше время и Numl действительно в этом преуспевает, абстрагируя от вас сложные технические решения. Это даёт вам больше времени, чтобы сфокусироваться на продукте, а не воевать с тултипами.  Проект находится в статусе PRE-BETA (v0.11). Это означает, что бОльшая часть синтаксиса стабилизирована, однако отдельные вещи всё еще находятся в статусе experimental и могут не попасть в релиз v1 (например, валидация форм). Релиз намечен на осень 2020. Немного статистики: Если вы заинтересовались проектом Numl, то мы будем очень рады любой поддержке, будь то звёздочка на гитхабе, пост в соцсетях или визит в наш уютный чатик в телеграме, где можно задать любой вопрос про проект. Только помощь сообщества (а может именно ваша!) может помочь вырасти этому проекту и составить конкуренцию другим популярным подходам. Спасибо за внимание! Буду рад ответить на вопросы. Еще раз все ссылки в одном месте: Проекты на Numl: В комментарии опишите небольшую задачу для вёрстки и я пришлю вам ссылку на REPL, где она будет решена с помощью Numl. Вот несколько примеров, которые работают с помощью возможностей языка, доступных из коробки, без использования дополнительного JS или CSS: CSS Cheater, DX Advocate