Как Nordhealth использует пользовательские свойства в веб-компонентах

Преимущества использования пользовательских свойств в системах проектирования и библиотеках компонентов.

Дэвид Дарнс
David Darnes

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

Как мы создаем веб-компоненты

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


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`
; } } customElements.define('simple-greeting', SimpleGreeting);
Веб-компонент, написанный с помощью Lit.

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


// TODO: DevSite - Code sample removed as it used inline event handlers
Использование веб-компонента, созданного выше на странице.

Инкапсуляция стиля Shadow DOM

Примерно так же, как собственные HTML-элементы имеют Shadow DOM , то же самое можно сказать и о веб-компонентах. Shadow DOM — это скрытое дерево узлов внутри элемента. Лучший способ визуализировать это — открыть веб-инспектор и включить опцию «Показать теневое дерево DOM». Сделав это, попробуйте просмотреть собственный элемент ввода в инспекторе — теперь у вас будет возможность открыть этот ввод и просмотреть все элементы внутри него. Вы даже можете попробовать это с одним из наших веб-компонентов — попробуйте проверить наш пользовательский компонент ввода, чтобы увидеть его теневой DOM.

Теневой DOM проверен в DevTools.
Пример Shadow DOM в обычном элементе ввода текста и в нашем веб-компоненте ввода Nord.

Одним из преимуществ (или недостатков, в зависимости от вашего мировоззрения) Shadow DOM является инкапсуляция стилей. Если вы пишете CSS внутри своего веб-компонента, эти стили не могут выйти наружу и повлиять на главную страницу или другие элементы; они полностью содержатся внутри компонента. Кроме того, CSS, написанный для главной страницы или родительского веб-компонента, не может проникнуть в ваш веб-компонент.

Такая инкапсуляция стилей является преимуществом нашей библиотеки компонентов. Это дает нам большую гарантию того, что когда кто-то использует один из наших компонентов, он будет выглядеть так, как мы задумали, независимо от стилей, примененных к родительской странице. И чтобы еще больше убедиться, добавляем all: unset; в корень или «хост» всех наших веб-компонентов.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Некоторый шаблонный код компонента применяется к теневому корню или селектору хоста.

Однако что, если у кого-то, использующего ваш веб-компонент, есть законная причина изменить определенные стили? Может быть, есть строка текста, которая требует большего контраста из-за контекста или граница должна быть толще? Если никакие стили не могут проникнуть в ваш компонент, как вы можете разблокировать эти параметры стиля?

Вот тут-то и приходят на помощь пользовательские свойства CSS.

Пользовательские свойства CSS

Пользовательские свойства названы очень правильно — это свойства CSS, которым вы можете полностью дать имя и применить любое необходимое значение. Единственное требование — поставить перед ними два дефиса. После того как вы объявили свое пользовательское свойство, его значение можно использовать в CSS с помощью функции var() .


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Пример из нашей CSS Framework токена дизайна в качестве пользовательского свойства, который используется во вспомогательном классе.

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

Эта возможность наследовать пользовательские свойства с использованием функции var() позволяет нам проникнуть сквозь теневую модель DOM наших веб-компонентов и предоставить разработчикам более детальный контроль над стилизацией наших компонентов.

Пользовательские свойства в компоненте Nord Web

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


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Пользовательские свойства CSS определяются в корневом селекторе.

Эти значения токенов затем используются в наших компонентах. В некоторых случаях мы применяем значение непосредственно к свойству CSS, но в других мы фактически определяем новое контекстное пользовательское свойство и применяем к нему значение.


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
Пользовательские свойства определяются в теневом корне компонента, а затем используются в стилях компонента. Также используются пользовательские свойства из списка токенов дизайна.

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


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
Контекстное пользовательское свойство дополнения группы вкладок используется в нескольких местах кода компонента.

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


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Вариант компонента вкладки, в котором отступы изменяются с помощью одного обновления пользовательского свойства, а не нескольких обновлений.

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


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Использование компонента группы вкладок на странице и обновление пользовательского свойства заполнения до большего размера.

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

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

Дальнейшее развитие пользовательских свойств

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

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


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Два экземпляра нашего компонента-разделителя, которым нужны две разные цветовые обработки. Один из них вложен внутри раздела, который мы можем использовать для более конкретного селектора, но нам нужно конкретно настроить разделитель.

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

Частные и общедоступные пользовательские свойства

Частные пользовательские свойства — это то, что было создано Леа Веру и представляет собой контекстное «частное» пользовательское свойство самого компонента, но установленное в «публичное» пользовательское свойство с резервным вариантом.



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
CSS-разделитель веб-компонента с контекстными настраиваемыми свойствами настроен так, что внутренний CSS опирается на частное настраиваемое свойство, для которого установлено общедоступное настраиваемое свойство с резервным вариантом.

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


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Снова два разделителя, но на этот раз цвет разделителя можно изменить, добавив контекстное пользовательское свойство разделителя в селектор разделов. Разделитель унаследует его, создавая более чистый и гибкий фрагмент кода.

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

Надеюсь, эта информация о том, как мы используем веб-компоненты с настраиваемыми свойствами CSS, оказалась для вас полезной. Дайте нам знать, что вы думаете, и если вы решите использовать какой-либо из этих методов в своей работе, вы можете найти меня в Твиттере @DavidDarnes . Вы также можете найти Nordhealth @NordhealthHQ в Твиттере, а также остальных членов моей команды, которые усердно работали над объединением этой системы дизайна и реализацией функций, упомянутых в этой статье: @Viljamis , @WickyNilliams и @eric_habich .

Изображение героя Дэна Кристиана Падурца.