Всем привет! Меня зовут Андрей, я профессионально разрабатываю веб-интерфейсы уже больше 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", а список его особенностей в контексте создания больших и сложных интерфейсов:


  • CSS является достаточно низкоуровневым языком. Да, там есть такие крутые высокоуровневые спецификации как Grid, но бОльшая часть языка это примитивы и часто одна задача требует использования нескольких из них одновременно. Например для того, чтобы спозиционировать элемент над другим элементом по середине (тот же тултип), надо использовать одновременно position, top/right/bottom/left и transform, плюс обязательно надо добавить немного JS, чтобы тултип не убежал за экран.
  • В CSS много свойств, которые одновременно могут использоваться для решения разных задач. Например, box-shadow (внутренняя/внешняя тень или красивый бордер), transform (смещение и масштабирование) и т.п. Это может быть также неудобно, как одна JS-функция, которая решает несколько задач.
  • Специфичность селекторов и приоритизация стилей с помощью порядка в коде до сих пор создают проблемы, особенно для новичков. Про !important я промолчу.
  • Привязка свойств к состояниям элемента может быть очень простой, но непредсказуемой. Приоритет стилей для .cls:hover и .cls:focus зависит от порядка и в более сложных случаях от специфичности. Чтобы добавить стиль в состояние, мы должен убедиться, что он не конфликтует со стилями из другого состояния. Но можно писать предсказуемый CSS (.cls:hover:not(:focus) и т.п.), но мы получим очень комплексный синтаксис, в котором легко запутаться, и который потребует огромного рефакторинга в случае добавления нового состояния. В реальных проектах мы обычно сталкиваемся со смешанным подходом, который старается минимизировать кол-во кода, что дополнительно усложняет его поддержку.
  • Из предыдущего пункта также следует, что переопределения набора стилизованных состояний у элемента становится экспоненциально сложной задачей. А это значит, что наследование стилей с помощью добавления класса (.btn.fancy-btn) в общем случае не работает, даже если мы используем "предсказуемый подход". Мы не можем создать и применить универсальный класс, который бы, к примеру, сказал "убери/замени все стили связанные с состоянием hover для этого элемента". Достаточно банальная задача дизайна "замена набора состояний", (например для оптимизации под touch-устройства) не имеет универсального и простого решения через CSS.
  • CSS Media Queries имеют чрезмерно мощный и запутанный (@media not all and (hover: none)) синтаксис для тех задач, для которых мы их используем.
  • Интерфейс создаётся с помощью CSS+HTML, которые в браузере превращаются в сложную связку CCSOM+DOM с кучей правил. Это даёт огромную гибкость, которую мы так любим, но также создаёт простор для ошибок и появления багов, усложняя создание и поддержку кода.
  • В отличие от JS, контролировать качество CSS-кода очень сложно. Его крайне тяжело статически анализировать. А в runtime получение хоть какой-то информации о стилях элемента требует вызова getComputedStyle(), что влияет на производительность.
  • 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). Больше не требовалось переключение между контекстами разметка/стили для вёрстки страниц и составных компонентов.


Кол-во свойств БЭ росло, всё больше помогая быстро решать локальные задачи вёрстки. Но появились и первые проблемы.


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


<template>
    <div :class="classes" :style="styles">
        <slot></slot>
    </div>
</template>

Ура, первый кусочек кода!


Т.е. никакой композиции других элементов внутри нет. Композицией мы занимаемся на верхнем уровне. Пример для наглядности:


<my-btn>
    Button
    <my-popup>Popup content</my-popup>
</my-btn>

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


В этот момент стало очевидно, что подход очень удобен и надо думать, как его развивать, чтобы добавить адаптивность, контекстные стили, стили для состояний, более удобную работу с цветом (у нас в проекте было много раскрашенных элементов). Примерно в это время я давал мастер-класс по гридам и для упрощения демок создал простенький веб-компонент my-grid, который брал атрибуты и мапил их на стили. Я был поражен насколько хорошо мой подход вписался в концепцию Custom Elements. Ведь, в них по умолчанию нет никакой композиции! Следующие несколько дней я потратил на миграцию элементов нашего проекта на Custom Elements, а сами элементы вынес в отдельный Open Source проект NUDE Elements, название которого позже было сокращено до Numl. Все элементы получили префикс nu-.


Справка: NUDE – это название JS-фреймворка, на котором основан Numl и который был специально для него написан.

Миграция прошла на удивление легко. И началась активная работа над Numl параллельно основному проекту. В первую очередь нужно было избавиться от inline-стилей. Это сильно ограничивало возможности CSS и потребляло лишнюю мощность на маппингах. Таким образом был создан механизм генерации CSS в рантайме. Если упрощенно, то работало это следующим образом: Элемент nu-grid получал в атрибут columns значение 1fr 1fr. Генератор анализирует это, вызывает функцию columnsAttr('1fr 1fr'), которая выглядит следующим образом:


export function columnsAttr(val) {
    return {
        'grid-template-columns': val,
    };
}

Дальше генератор берёт результат и создаёт на его основе CSS:


nu-grid[columns="1fr 1fr"] {
    grid-template-columns: 1fr 1fr;
}

… и вставляет это всё в <style>, который в свою очередь вставляется в <head>. Разумеется алгоритм проверял, есть ли уже идентичный кусок CSS, чтобы не создавать дубликат.


Ожидаемого ускорения сайта это не принесло (всё и до этого работало мгновенно), но и медленнее не стало, а мы получили отличный механизм, который позволил пользоваться всеми благами CSS.


Может показаться, что это очень ресурсоёмко, но такой атомарный подход позволяет существенно сэкономить на кол-ве самого CSS (спросите адептов Atomic CSS), а некоторые адепты CSS-in-JS могут с удовольствием вам рассказать, что динамическая генерация CSS не такой уж сильный оверхед.


Добавляем новые возможности


Получив в свой арсенал полноценный CSS, я стал добавлять всё новые и новые свойства, по возможности упрощая их синтаксис.


Например, Numl обзавёлся дефолтными значениями для свойств. Это оказалось довольно удобно, ведь за простым <nu-block border> может скрываться достаточно выразительная вещь вроде:


nu-block[border] {
    border: var(--nu-border-width) solid var(--nu-border-color);
}

, где мы выставляем бордер с толщиной и цветом по умолчанию.


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


<nu-card>
    <nu-el place="outside-top">Float element</nu-el>
</nu-card>

Открыть в REPL


То же свойств можно использовать для позиционирования внутри grid/flex-раскладки, для float-позиционирования, fixed и sticky. В CSS эти свойства всё равно взаимоисключающие.


Иные свойства наоборот разделились. Например, transform стало возможно использовать оперируя разными свойствами:


<nu-card move="2rem 2rem" scale="2">
    Card
</nu-card>

Открыть в REPL


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


<nu-block width="10x"></nu-block>

Открыть в REPL


Где x — базовый gap.


Кстати о gap'ах. Мне очень нехватало его для Flexbox, и я решил его добавить:


<nu-flex gap="2x 1x" flow="row wrap">
    <nu-block>Item 1</nu-block>
    <nu-block>Item 2</nu-block>
    <nu-block>Item 3</nu-block>
</nu-flex>

Открыть в REPL


Подробнее можно посмотреть в Storybook.


Идей было очень много. Возможность создавать сложные стили и прятать их за простой API манила и было уже очень сложно остановиться. В какой-то момент возможности стилей Numl стали настолько продвинутыми, что стилизация всех элементов была на них переписана. Однако, стилями возможности Numl не ограничились.


Где же классы?


То, что у меня получилось, вполне можно назвать атомарным подходом. Однако, у такого подхода есть один большой недостаток — отсутствие возможности стилизовать несколько элементов единой декларацией. В CSS мы делаем это с помощью классов и это кажется настолько естественным и привычным, что отказ от классов может показаться безумием. Однако, после 4 месяцев работы с подобным подходом, оказалось, что классы слегка переоценены, ведь за редким исключением, я не получил никакого дискомфорта от атомарного подхода. В композиционных фреймворках есть два отличных решения для такой задачи: циклы и выделение в компонент.


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


Решить эту проблемы было отличным вызовом для Numl. Так появился первый элемент-объявление <nu-attrs>, который задаёт нужные атрибуты для элементов с определённым именем в контексте элемента-родителя. Причем атрибуты могут быть не только те, что используются для стилизации. Вот пример, как это работает:


<nu-pane>
    <nu-attrs for="btn" color="special"></nu-attrs>
    <nu-btn>Button 1</nu-btn>
    <nu-btn>Button 2</nu-btn>
</nu-pane>

Открыть в REPL


После такого объявления, каждая кнопка получила атрибут color="special". Более того, если вы динамически начнёте менять (или даже удалять!) атрибуты у <nu-attrs>, то же самое начнёт происходить и с кнопками. Атрибуты различных <nu-attrs> применяются каскадом, поэтому вы можете применить объявление даже к корневому элементу, это не помешает работе других объявлений уровнем ниже. В случае коллизии, значение атрибута из более близкого по иерархии объявления будет приоритетнее.


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


Узнать больше про <nu-attrs>и посмотреть примеры можно вот тут.


Добавляем красок с помощью тем


Я достаточно давно мечтал реализовать механизм для раскрашивания сайтов. Идея была в том, чтобы на основании минимального кол-ва информации генерить полноценные темы, с тёмным вариантом, чтобы пользователям было комфортно пользоваться интерфейсом по вечерам и при слабом освещении. Идеально было бы передавать некий оттенок (число от 0 до 359 в HSL) и получать набор Custom Properties для раскрашивания всего сайта, в котором цвета уже имеют необходимую насыщенность и контрастность.


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



Модуль юнит-экономики. Sellerscale


Звучит интересно, но по факту это несёт в себе уйму технических и дизайнерских вызовов:


  • Надо найти решение для генерации цветов по унифицированной яркости
  • Нужно написать алгоритм для вычисления яркости по относительной контрастности входного цвета. (по WCAG Contrast Ratio).
  • Нужно понять, какие возможности кастомизации предусмотреть, чтобы удовлетворить дизайнерскую фантазию.
  • Разобраться как работает свет в плане восприятия его человеком.
  • Понять какой API должен быть у этой фичи.
  • Словить все подводные камни, но найти в себе силы продолжать.
  • Понять, какие цвета генерить и как их называть (пожалуй, самое сложное)

Эта фича заняла у меня три с лишним месяца, было сделано шесть крупных итераций. В конечном итоге получилась очень удобная система, в которую удалось добавить даже Режим высокой контрастности. Как это работает в простом виде можно посмотреть на моей домашней страничке: tenphi.me (кнопочки настройки темы наверху) или в меню настроек в Storybook. Также есть интерактивная демка с градиентами на CodePen написанная чисто на Numl.



Раздел с демонстрацией механизма тем на лэндинге numl.design



То же самое в тёмном варианте



Окно настроек темы в Storybook


Разумеется, можно использовать и отдельные цвета, для этого была создана кастомная функция hue(), с помощью, которой можно генерить адаптивные цвета, задавая их в выдуманном специально для Numl переменном цветовом пространстве HSC (Оттенок, Насыщенность, Контрастность). Градиент на моей домашней страничке сделан как раз с помощью этой функции.



Моя домашняя страничка. Демонстрация цветовых возможностей Numl. tenphi.me


Благодаря темизации Numl превратился в довольно продвинутую Дизайн-систему, которую легко кастомизировать под себя.



Я планирую написать отдельную большую статью про алгоритм темизации, и поделиться решением, которым можно было бы использовать отдельно от Numl. А сейчас возвращаемся к нашим элементам...


Адаптивность (или отзывчивость)


Следующим вызовом было добавление мобильной версии для нашего сайта. К счастью, к тому моменту у меня в голове уже была готовая спецификация для этого. Выглядело (и выглядит до сих пор) это примерно так: мы не используем глобальные breakpoint'ы, вместо этого мы их объявляем в контексте конкретного элемента, разделяя символом | (вертикальная черта), причем кол-во точек не ограничено. Это позволяет нам менять breakpoint'ы на любом уровне, а его дочерние элементы эти точки наследуют:


<nu-root responsive="60rem|40rem">
    ...
</nu-root>

Две точки адаптивности создают три зоны значений для каждого свойства (также, как две точки на отрезке делят его на три части). Теперь нужно эти значения как-то различать в свойствах. К счастью, для этого есть всё тот же символ |:


<nu-root responsive="60rem|40rem">
    <nu-grid columns="repeat(4, 1fr)|1fr 1fr|1fr">
        ...
    </nu-grid>
</nu-root>

Открыть в REPL


Так мы определили значение для каждой зоны. Если значение при переходе точки не меняется, просто пропускаем его. Чуть подробнее можно почитать тут.


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



Скриншоты мобильной версии Sellerscale


Привязываем стили к состояниям


Разумеется, чтобы система была достаточно гибкой, в ней должна присутствовать возможность привязывать стили к различным состояниям объекта. Также, как мы знаем, состояния могут быть и кастомными (вспомните модификаторы из БЭМ), поэтому всё должно быть максимально гибко.


Я остановился на следующем синтаксисе:


<nu-card shadow="0 :hover[1]">
    Content
</nu-card>

Открыть в REPL


Тут мы говорим, что у элемента нет тени в базовом состоянии, но она появляется, когда мы наводим курсор.


Можно привязать стиль и к родителю, например:


<nu-card>
    <nu-block color="^ text :hover[special]">Content</nu-block>
</nu-card>

Открыть в REPL


Есть и другие чуть более сложные варианты синтаксиса. Прелесть здесь в том, что внутренний алгоритм создаёт взаимоисключающие селекторы для каждого стиля:


nu-card[color="^:hover[special]"]:hover {
    color: var(--nu-spcial-color);
}
nu-card[color="^:hover[special]"]:not(:hover) {
    color: var(--nu-text-color);
}

Не так впечатляюще выглядит при двух значениях. А что если их три или четыре? Numl найдёт селектор для любой их комбинации и вставит значение. Если значение не описано, то он выставит для него базовое значение (в примере выше это text). Этот алгоритм позволяет использовать различные значения CSS свойств для одного стиля, не боясь, что они перемешаются! Ну и браузер, уверен, будет благодарен, что ему не нужно разбираться с коллизией стилей.


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


<nu-btn is-loading>
    <nu-el show="^ y :loading[n]">Submit</nu-el>
    <nu-icon name="loader" show="^ n :loading[y]"></nu-icon>
</nu-btn>

Открыть в REPL


Промежуточный итог


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


  • Стили были заменены на более высокоуровневыми объявления, каждое из которых решает конкретную задачу и имеет более выразительный синтаксис, который можно легко расширять на своё усмотрение.
  • Специфичность и порядок применения стилей разруливается "под капотом", не надо об этом задумываться.
  • Привязка стилей к состояниям элемента стала простой и предсказуемой.
  • Переопределение списка состояний для конкретного стиля элемента теперь работает. К примеру, мы можем без труда добиться, чтобы конкретный элемент перестал изменяться при переходе в состояние hover. Мы даже можем корректировать список состояний на уровне приложения, убирая, например, все стили для hover состояния на touch-устройствах. (сейчас данная фича вшита в Numl, но в будущем для этого может появиться отдельное API).
  • Адаптивность достигается без использования Media Queries.
  • Numl легче поддаётся статическому анализу. А в runtime мы можем с помощью селекторов и атрибутов получать очень много информации, о том, что из себя представляет конкретный элемент, без необходимости вызывать дорогой getComputedStyle(). Всё это позволяет при желании жестко контролировать качество вёрстки.
  • Все стили спрятаны за абстракцией.Каждое свойство для стилизации выполняет свой собственный контракт, делая вёрстку предсказуемой и уменьшая шанс появления ошибок и багов. Внутренняя реализация может меняться вместе с развитием технологий.

Звучит невероятно, но я дважды проверял, всё действительно так. :)


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


Поведения. Оживляем элементы


Мы абстрагировали сложные стили за простой абстракцией, но что если сделать то же самое для базовых поведений? Например, в HTML, когда мы вставляем обычную кнопку <button> она уже из коробки умеет нажиматься и держать фокус.


Чтобы сделать аналогичный механизм в Numl, ничего придумывать не пришлось. Есть достаточно широко применяемые директивы во фреймворках. В Numl была реализована их альтернатива под названием Behaviors. Работает это просто. Берём нужное поведение и инжектируем его с помощью атрибута, добавляя префикс nx- к его названию.


<nu-el nx-action>Button</nu-el>

Открыть в 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. Скорее всего, это именно то, что вы ожидаете.


<nu-region labelledby="label">
    <nu-block id="label"></nu-block>
</nu-region>

Открыть в REPL


В отличие от обычного HTML, мы можем размножить данный кусок кода, не ломая доступность.


В Numl большинство aria-атрибутов не используются напрямую. Чтобы, например, добавить описание для элемента, надо использовать сокращённую форму такого атрибута (без aria-), как в примере выше.


Сокращенные атрибуты удобны и в других ситуациях:


<nu-btn label="Turn on lights">
    <nu-icon name="sun"></nu-icon>
</nu-btn>

Опережу ваш вопрос. Нет, такой подход практически не вызывает коллизий.


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


Итоги


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


  • Время инициализации фреймворка. Ядро Numl в текущей версии весит 40кб, что, конечно, неслабо. Однако, по мере роста проекта это может даже положительно сказываться на общем размере бандла, за счёт того, что стили становятся намного лаконичнее, а некоторые возможности Numl позволяют избавиться от большого кол-ва JS кода. Уже в следующей версии размер ядра будет существенно уменьшен благодаря оптимизации кода и асинхронной подгрузке.
  • Время рендеринга. Каждый отдельный элемент в Numl имеет свою логику, что уменьшает время рендеринга, особенно изначального, когда генерится много стилей. Однако, боевое испытание на большом проекте показало, что метрики UX от этого не страдают.
  • SSR. Numl совместим с SSR техникой, но на сервере выполняться не может. Поэтому корректные роли выставлены не будут. Если вам нужно SEO для поисковика помимо гугла, то потребуется использование решения вроде prerender.io.
  • Поддержка браузеров. Numl совместим только с современными браузерами, которые поддерживают Custom Elements, Custom Properties и CSS Grid. Ранние версии Numl поддерживали Edge 15+ с полифилом, но позже от поддержки пришлось отказаться (не было на это ресурсов).
  • Numl резервирует большое кол-во свойств, что может создавать коллизию со свойствами компонента на уровне JS-фреймворка. Хотя сам факт коллизии не несёт опасности, но может усложнить понимание кода. В реальном проекте никаких проблем с этим не возникло.
  • На данный момент Numl это инди-проект с маленьким комьюнити, за которым не стоит большая компания. Его будущее сейчас целиком в руках комьюнити.

С другой стороны, Numl имеет очень сильные стороны, которые могут заинтересовать как начинающих разработчиков, так и опытных гуру:


  • Возможность очень быстро разрабатывать качественные интерфейсы. Создав прототип, не нужно его переписывать "по-нормальному", можно взять как есть, добавить всю необходимую логику и катить в прод.
  • Отличный DX, возможность не переключаться между контекстами во время создания прототипа-интерфейса. Всё можно описать используя лишь HTML, даже простые взаимодействия. (скрыть/показать/передать значение/изменить атрибут)
  • Возможность копировать вёрстку "как есть" из одного проекта в другой, сохраняя внешний вид интерфейса и не привязываясь к фреймворку.
  • Очень дешевая поддержка вёрстки. Не нужно переживать за "мёртвый CSS". Легче контролировать качество вёрстки.
  • Возможности стилизации превосходят все известные мне CSS-фреймворки (включая популярный TailwindCSS) и могут легко расширяться, сохраняя такие фичи как адаптивность и привязка стилей к состояниям.
  • Уникальная система темизации и адаптивных цветов, которая будто прилетела прямиком из прекрасного будущего.
  • Дизайн-система из коробки, в которой всё настраивается до мелочей.
  • Соточка в лайтхаусе возможна ;)

Хороший инструмент должен уважать ваше время и Numl действительно в этом преуспевает, абстрагируя от вас сложные технические решения. Это даёт вам больше времени, чтобы сфокусироваться на продукте, а не воевать с тултипами.



Заключение


Проект находится в статусе PRE-BETA (v0.11). Это означает, что бОльшая часть синтаксиса стабилизирована, однако отдельные вещи всё еще находятся в статусе experimental и могут не попасть в релиз v1 (например, валидация форм). Релиз намечен на осень 2020.


Немного статистики:


  • Проекту уже больше года
  • 1100+ коммитов
  • 200+ версий было выпущено в npm
  • ~1300 человекочасов вложено в проект
  • 85 звёздочек на GitHub (ой, уже больше, так и до сотни не далеко!)

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


Спасибо за внимание! Буду рад ответить на вопросы. Еще раз все ссылки в одном месте:



Проекты на Numl:



Небольшой интерактивчик для тех, кто дочитал до конца


В комментарии опишите небольшую задачу для вёрстки и я пришлю вам ссылку на REPL, где она будет решена с помощью Numl.


Вот несколько примеров, которые работают с помощью возможностей языка, доступных из коробки, без использования дополнительного JS или CSS: