Преимущества использования пользовательских свойств в системах проектирования и библиотеках компонентов.
Меня зовут Дэйв, я старший разработчик 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);
Но самое привлекательное в веб-компонентах — это то, что они работают практически с любым существующим JavaScript-фреймворком, а то и вовсе без него. После того, как на странице появляется ссылка на основной пакет JavaScript, использование веб-компонента очень похоже на использование нативного HTML-элемента. Единственный реальный признак того, что это не нативный HTML-элемент, — это постоянный дефис внутри тегов, который является стандартом, указывающим браузеру на то, что это веб-компонент.
Инкапсуляция в стиле Shadow DOM
Подобно тому, как нативные HTML-элементы имеют теневой DOM , он есть и у веб-компонентов. Теневой DOM — это скрытое дерево узлов внутри элемента. Лучший способ визуализировать это — открыть веб-инспектор и включить опцию «Показать теневое дерево DOM». После этого попробуйте взглянуть на нативный элемент ввода в инспекторе — теперь у вас будет возможность открыть этот элемент и увидеть все его элементы. Вы даже можете попробовать это с одним из наших веб-компонентов — попробуйте изучить наш пользовательский компонент ввода , чтобы увидеть его теневой DOM.

Одним из преимуществ (или недостатков, в зависимости от вашей точки зрения) теневого 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 или разработчик, желающий получить значение из нашего списка токенов.
Возможность наследовать пользовательские свойства с помощью функции 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-свойству, но в других случаях мы фактически определяем новое контекстное пользовательское свойство и применяем значение к нему.
: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-sp
ace-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);
/* ... */
}
Определение наших контекстных пользовательских свойств таким образом означает, что мы по-прежнему можем делать все то, что делали раньше, например, наследование глобальных значений токенов и повторное использование значений в коде нашего компонента; но компонент также будет корректно наследовать новые определения этого свойства для себя или любого родительского элемента.
<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 .
Изображение героя Дэна Кристиана Падурца.