Преимущества использования пользовательских свойств в системах проектирования и библиотеках компонентов.
Меня зовут Дейв, я старший фронтенд-разработчик в компании 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, есть Shadow DOM. Shadow DOM – это скрытое дерево узлов внутри элемента. Чтобы увидеть это, откройте веб-инспектор и включите параметр Show Shadow DOM tree (Показать дерево Shadow DOM). После этого попробуйте посмотреть на встроенный элемент ввода в инспекторе – теперь у вас будет возможность открыть этот ввод и увидеть все элементы внутри него. Вы можете попробовать сделать это с одним из наших веб-компонентов. Например, проверьте наш специальный компонент ввода, чтобы увидеть его Shadow DOM.
Одно из преимуществ (или недостатков, в зависимости от вашей точки зрения) 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. Любое пользовательское свойство, примененное к родительскому элементу или к самому элементу, можно использовать в качестве значения для других свойств. Мы активно используем пользовательские свойства для наших токенов дизайна, применяя их к корневому элементу с помощью нашего фреймворка CSS. Это означает, что все элементы на странице могут использовать эти значения токенов, будь то веб-компонент, вспомогательный класс CSS или разработчик, желающий получить значение из нашего списка токенов.
Возможность наследовать дополнительные параметры с помощью функции var() позволяет нам проникать в Shadow DOM веб-компонентов и предоставлять разработчикам более точный контроль над стилями компонентов.
Пользовательские свойства в веб-компоненте Nord
При разработке компонентов для нашей системы проектирования мы тщательно продумываем 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-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>
Причина, по которой вам нужно задать значение дополнительного параметра непосредственно в компоненте, заключается в том, что мы определяем их в одном и том же элементе, используя селектор хоста компонента. Глобальные токены дизайна, которые мы используем непосредственно в компоненте, проходят напрямую, не затрагиваются этой проблемой и могут даже перехватываться родительскими элементами. Как нам получить лучшее из обоих миров?
Частные и общедоступные дополнительные параметры
Личные пользовательские свойства – это концепция, разработанная Леа Веру (Lea Verou). Она представляет собой контекстное "личное" пользовательское свойство в самом компоненте, но заданное как "общедоступное" пользовательское свойство с резервным вариантом.
: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.