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

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

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

Меня зовут Дэйв, я старший разработчик front-end-приложений в 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-элемент, — это постоянный дефис внутри тегов, который является стандартом, указывающим браузеру на то, что это веб-компонент.

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

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

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

Одним из преимуществ (или недостатков, в зависимости от вашей точки зрения) теневого 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-структуры токена дизайна как пользовательского свойства, используемого во вспомогательном классе.

Что касается наследования, все пользовательские свойства наследуются, что соответствует типичному поведению обычных свойств и значений CSS. Любое пользовательское свойство, примененное к родительскому элементу или к самому элементу, может быть использовано в качестве значения других свойств. Мы активно используем пользовательские свойства для наших дизайн-токенов, применяя их к корневому элементу через наш CSS-фреймворк. Это означает, что все элементы на странице могут использовать эти значения токенов, будь то веб-компонент, вспомогательный класс 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="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup

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

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

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

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

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

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


<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 .

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