Numl – Альтернативный язык разметки и стилизации для веб

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


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

    Спасибо, что прочитали статью. Захотелось ли вам попробовать поверстать на Numl?

    • 10,8%Да, очень хочу попробовать! / Уже пробую!18
    • 24,6%Проект интересный, но для меня не актуален.41
    • 16,8%Подожду сторонних обзоров, прежде чем пробовать.28
    • 13,2%Подожду релиза и попробую.22
    • 31,7%Проект мертворожденный, пробовать нет смысла.53
    • 3,0%*Ушел пилить клон Numl*5
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 33

      +4
      Нужно больше жирного текста.
        +2
        В Waterfox с noscript и разрешенными скриптами (numi и даже, на всякий случай, яндекса) примеры не отобразились. Просто серый экран. В Opera с разрешенным всем заработало только после нажатия на кнопки масштаба. Иначе правая часть пустая. Вот за это я и не люблю скрипты.
        Понятно, что без них сейчас никуда, однако когда без них вообще всё не работает — это так себе.

        Upd: домашняя страница тоже пустая со включенными скриптами. Без — показывает юзерагент.
          +2
          Custom Elements не работают без скриптов в принципе. Если есть необходимость создания JS-free решения, то это возможно на базе Numl. Нужно просто все стили и вёрстку (да, там используются внутренние элементы элементы для ссылок, картинок и т.п.) предварительно пререндерить. Я делал такой эксперимент и всё работало, даже навигация. Но более сложные взаимодействия там конечно умрут, например формы.

          Но можно пойти еще дальше и сделать транспиляцию Numl в HTML. Тогда будет работать вообще всё, что может работать без JS.

          У меня нет ресурсов на подобные эксперименты. Но я всегда рад контрибьютерам, которые приходят с интересными идеями и помогают их реализовать.
            0
            Что совсем без скриптов не работают — это очевидно. А вот почему совсем не завелось со включенными — это непонятно. И непонятно вообще что происходит, т.к. просто пустой экран. Думал — может адблок что ломает, но его отключение ничего не изменило. Все-таки пререндер — более предпочтительно, хотя (скорее всего) гораздо менее экономно по трафику.
            +2
            Если есть проблемы с тем, что из коробки с включенными скриптами Numl не работает в некоторых браузерах, то буду очень благодарен за issue на гитхабе. Уверен, я смогу всё починить, получив подробную информацию.
            0
            Upd2:
            На домашней если нажать сразу на месяц в опере (с разрешенным всем) переключается на ч/б и зависает в этом режиме намертво.
              +1
              В Angular это называется Attribute directives. С помощью них сделаны многие популярные библиотеки, например Flex-Layout. Соответственно поддерживается и пререндеринг всего этого и динамическое обновление.
                –3
                Я правильно понимаю, что, чтобы сделать хороший интерфейс, мне нужно поставить Angular, найти несколько десятков популярных библиотек под каждую мелкую задачку, разобраться как они работают и подключаются? А что если заказчик попросит переехать на React и придётся искать новый набор библиотек? Вам самому не кажется, что что-то тут не так?

                Если уж искать альтернативу, то лучше что-то на Web Components.

                С динамическим обновлением у Numl проблем нет.

                Интересно, какие проблемы вы решаете пререндерингом?
                  +3
                  А что если заказчик попросит переехать на React и придётся искать новый набор библиотек?

                  А что если заказчик попросит вас переехать с numl на react?
                  angular поддерживает web components.
                    +1
                    Numl не альтернатива React'у, не нужно между ними переезжать. Numl решают свою задачу связанную с созданием качественной вёрстки. Это единый инструмент для решения большого спектра задач, с которыми композиционные фреймворки не сильно дружат – доступность, управление цветам, консистентный дизайн и т.п. С другой стороны, Numl плохо работает с композицией и состоянием, поэтому не стоит с помощью него решать такие задачи.

                    Однако, в некоторых простых задачах Numl может заменить JS-фреймворк, если вам нужно динамически менять любые стили или атрибуты на элементах (например, показывать/прятать элемент), а также корректировать надписи.
                0

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

                  0
                  Это не замена Angular. Это скорее UI-библиотека, которая работает параллельно вашему JS-фреймворку. Она решает задачи на уровне вёрстки, а композиционные задачи и работу с состоянием оставляет другим технологиям, которые начились это делать по-настоящему круто.
                  Numl можно использовать с чистым HTML или с любым популярным фреймворком — Angular, Vue, Svelte, React.
                    +4
                    надеюсь, быстрее чем он

                    Разрабатываю на angular с 2 версии по 10, не сталкивался с проблемами производительности. Думаю в любом фреймворке/библиотеке проблемы производительности из-за прослоек между креслом и монитором.
                    0
                    1. CSS-синтаксис внутри аттрибутов — строка. В IDE это будет сплошным цветом, а я хочу видеть нормальную подсветку с автоподстановкой.
                    2. Почему отказались от синтаксиса shadow:hover[:1]="0"?

                    + можно сделать синтаксис: (shadow="0" size="2rem"):hover[:1]

                    Я так понял, что речь идет не про альтернативный язык, а про просто библиотеку компонентов.

                    Мне нравится идея задавать стиль на самом компоненте, но по-сути это тот же самый style="...". Ну погранулярней, ну можно задавать псевдоклассы.

                    КМК, если делать такой ход конем:

                    <button>
                      <style>
                        :self {
                          box-shadow: 1px;
                        }
                      </style>
                    </button>
                    


                    Это будет и покрасивее и поудобней. Почему так не стали?
                      +1
                      Спасибо за толковый комментарий!

                      1. Поддержку IDE всегда можно добавить при желании и будет подсвечиваться. Хотя в данном случае я согласен, что реализовать адекватную подсветку будет сложно. В целом синтаксис достаточно понятный и без подсветки. Значения CSS свойств например почти никто не подсвечивает, а там синтаксис ну очень комплексный может быть. И вроде это не вызывает проблем.
                      2. Во-первых, такое название атрибута не поддерживается спецификацией Custom Elements, я даже не уверен, что ваши примеры это валидный HTML. Во-вторых, такой синтаксис менее очевидный и сложнее мапится на CSS.

                      Можно рассматривать Numl как библиотеку элементов, но по факту это язык описания семантики + язык описания стилей + фреймворк, который позволяет инжектировать поведения в элементы и даёт им возможность понимать свой контекст. Также тут скорее UI-библиотека, но более правильное название — HTML Framework.

                      По сути, это ближе к атомарный CSS-фреймворкам вроде Tailwind, только мощнее по возможностям благодаря кодогенерации.

                      Про кусочек кода:
                      Я не стал так делать, 1) потому что это обычный CSS со всеми особенностями описанными в статье. 2) это очень длинное и немасштабируемое описание стилей.
                        +2
                        Было уже много фреймворков, которые пытались делать свой синтаксис внутри атрибутов и это всегда боль. Вы пишете что «Поддержку IDE всегда можно добавить», но по факту, очень мало библиотек имеют такую поддержку. Я знаю только Angular, разве что, но это топ-3 фреймворк. Если поддержка синтаксиса появится когда Numl войдет в первую тройку, ждать придется очень долго.

                        Дальше вы делаете очень много утверждений, которые видны только из вашего опыта. Я не знаю, насколько ваш опыт типичен, но де-факто, сейчас уже смотрят на то, как живые люди пишут код. Фреймворки Angular, React, Vue (отчасти) сделаны, смотря на то, как реальные люди пишут много кода в больших компаниях. Я не знаю кто вы и где работаете, но если это ваше мнение построено не на анализе поведения живых людей, то практически без шансов. Конкурировать с одним только ангуляром сегодня чистое самоубийство а их там три с кепкой.
                          0
                          Numl не конкурент Angular, React и Vue. Он работает параллельно внутри вёрстки, а поверх вы можете использовать один из этих фреймворков (или другой, поддерживающий Web Components).

                          Подход Numl существенно отличается от других CSS-фреймворков и Дизайн-систем. Только время покажет, насколько людям будет удобно с ним работать в реальных проектах. Но по предварительным отзывам у людей очень приятный опыт его использования. Посмотрим, что будет дальше.
                            0
                            Я хотел бы, чтобы вы меня постарались понять.
                          +1
                          Значения CSS свойств например почти никто не подсвечивает, а там синтаксис ну очень комплексный может быть. И вроде это не вызывает проблем.

                          WebStorm подсвечивает и подсказывает CSS:


                          скриншот кода в котором IDE подсвечивает CSS

                            0
                            С подсказками проблем быть не должно. Их можно легко добавить в VSCode и сложно (но можно!) добавить в WebStorm.

                            В приведённом вами примере подсветка практически ничем вам не помогает, код и без этого хорошо читается. Но, разумеется, лучше когда она есть.
                      +1

                      Спасибо за картинку. Оказалась очень кстати. Так совпало, что я её увидел в момент когда писал родмап, по своему решению последней проблемы человечества, и в нём она отлично смотрится.

                        0
                        Да, я тоже её обожаю. Очень наглядная демонстрация текущим проблем с вебом. Многие базовые вещи требуют огромных усилий для корректной реализации.
                        0

                        Хотелось бы, конечно, иметь русскую версию документации для проекта, созданного русскоговорящими людьми. Я сам особых проблем с английским не имею, но, если делается упор на русскоязычное коммьюнити, это, имхо, must-have.

                          +2
                          Есть идея в будущем сделать краудфандинг переводов, чтобы люди могли сами добавлять переводы в документацию (например, на основе magic-string). Но пока проект не развился, сложно оправдать такую работу.

                          Я очень много сил и времени вложил в этот проект, потому что верю в него. Если появится хотя бы пара человек, которые меня в этом поддержат, то появятся и новые фичи и документация на разные языки и много чего еще :)
                          0

                          Вы написали коммерческий проект на своем новом языке разметки, но только вот я попробовал зайти на него с телефона Android 7 браузер Firefox последней версии. Страница с ценами просто не открывается — чистый лист. На главной верстка едет, появляется горизонтальный скролл у страницы. В Edge верстка сайта едет, как будто просто нет стилей. Сайтом не возможно пользоваться.


                          Исходя из этого возникает вопрос: что это за язык разметки и стилизации, если у него такая низкая совместимость с браузерами?


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

                            +1
                            Спасибо за вопрос.

                            Я не скрываю и даже подробно упоминаю, что Numl поддерживается только современными вечно-зелёными браузерами, хотя при желании можно добавить поддержку и более старых браузеров вплоть до Edge15.

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

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

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

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


                            Куча селекторов для одной только кнопки

                            image

                              0
                              Всё действительно так, если вы используете кучу селекторов, которые одновременно применяются, будет падение производительности. В случае, если селекторы пассивные (не работают) их отсеивание обойдётся браузеру относительно дёшево. Мы сделали достаточно большое приложение на Numl с очень большим кол-вом элементов и всё работает достаточно быстро.

                              Для каждого элемента Numl предварительно генерирует много селекторов, чтобы иметь возможность использовать конкретное CSS-свойство (например, `box-shadow) в нескольких стилях: `border/shadow/inset/mark`. Я надеюсь, что в будущем удастся еще сильнее оптимизировать этот подход.
                              +1
                              Отличная статья, приятно и структурировано написана.

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

                              В любом случаи, желаю удачи и очень интересно будет понаблюдать за развитием проекта.
                                0
                                И вам спасибо за отзыв!

                                По факту, Numl совместим с чистым HTML и CSS, вы можете перемешивать их использование, хоть это и не рекомендуется. Плюс, там, где это возможно, Numl повторяет синтаксис и именование HTML/CSS/ARIA, чтобы разработчик переиспользовал свои знания (в обе стороны). Пилить радикально отличающийся язык было бы неэффективно по многим причинам. Цель Numl сделать возможности CSS более доступными для разработчиков, а не обрезать его, как ИМХО, делают большинство Дизайн-систем.

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

                                Сравнивать Numl с другими фреймворками довольно сложно. В нём много возможностей, которых нет в других фреймворках. Некоторые возможности, такие как привязка стилей к состояниям, работают эффективнее, но это может быть не так очевидно, как хотелось бы. Тем более, чтобы качественно сравнить Numl с тем же Tailwind, нужен человек, который много писал на Tailwind, чтобы мы с ним вместе сели и расписали сильные и слабые стороны и сделали понятную таблицу сравнения.
                                0
                                Очень нужный проект и правильные идеи.
                                Только он должен быть как TS для JS, его не должно быть на клиенте в таком виде.
                                  0
                                  Спасибо за отзыв.

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

                                  Возможно в будущем, Numl сможет переползти на сервер и даже мобильные платформы. Посмотрим :)

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

                                Самое читаемое