Die Vorteile der Verwendung von benutzerdefinierten Eigenschaften in Designsystemen und Komponentenbibliotheken.
Ich bin Dave und Senior Front-End Developer bei Nordhealth. Ich arbeite am Design und an der Entwicklung unseres Designsystems Nord, was auch die Entwicklung von Webkomponenten für unsere Komponentenbibliothek umfasst. Ich möchte Ihnen gern zeigen, wie wir die Probleme beim Stylen von Webkomponenten mithilfe von benutzerdefinierten CSS-Eigenschaften gelöst haben und welche weiteren Vorteile die Verwendung benutzerdefinierter Eigenschaften in Designsystemen und Komponentenbibliotheken bietet.
So erstellen wir Webkomponenten
Wir verwenden Lit, eine Bibliothek, die viel Boilerplate-Code wie Status, bereichsbezogene Stile und Vorlagen für die Erstellung unserer Webkomponenten bereitstellt. Lit ist nicht nur schlank, sondern basiert auch auf nativen JavaScript-APIs. Das bedeutet, dass wir ein schlankes Code-Bundle bereitstellen können, das die Funktionen des Browsers nutzt.
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);
Das Beste an Web-Komponenten ist jedoch, dass sie mit fast jedem vorhandenen JavaScript-Framework oder auch ohne Framework funktionieren. Sobald auf das Haupt-JavaScript-Paket auf der Seite verwiesen wird, ist die Verwendung einer Webkomponente der Verwendung eines nativen HTML-Elements sehr ähnlich. Das einzige wirkliche Anzeichen dafür, dass es sich nicht um ein natives HTML-Element handelt, ist der durchgehende Bindestrich in den Tags. Das ist ein Standard, um dem Browser zu signalisieren, dass es sich um eine Webkomponente handelt.
Shadow-DOM-Stilkapselung
Webkomponenten haben ein Shadow DOM, genau wie native HTML-Elemente. Das Shadow-DOM ist eine verborgene Baumstruktur von Knoten innerhalb eines Elements. Am besten lässt sich das veranschaulichen, indem Sie den Webinspector öffnen und die Option „Show Shadow DOM tree“ (Shadow-DOM-Baum anzeigen) aktivieren. Wenn Sie das getan haben, sehen Sie sich ein natives Eingabeelement im Inspector an. Sie haben jetzt die Möglichkeit, die Eingabe zu öffnen und alle Elemente darin zu sehen. Sie können dies sogar mit einer unserer Webkomponenten ausprobieren. Sehen Sie sich das Shadow DOM unserer benutzerdefinierten Eingabekomponente an.

Einer der Vorteile (oder Nachteile, je nach Sichtweise) von Shadow DOM ist die Stilverkapselung. Wenn Sie CSS in Ihrer Webkomponente schreiben, können diese Stile nicht nach außen dringen und die Hauptseite oder andere Elemente beeinflussen. Sie sind vollständig in der Komponente enthalten. Außerdem kann CSS, das für die Hauptseite oder eine übergeordnete Webkomponente geschrieben wurde, nicht in Ihre Webkomponente gelangen.
Diese Kapselung von Stilen ist ein Vorteil in unserer Komponentenbibliothek. So können wir besser garantieren, dass eine unserer Komponenten so aussieht, wie wir es uns vorgestellt haben, unabhängig von den auf die übergeordnete Seite angewendeten Stilen. Um das zu gewährleisten, fügen wir all: unset;
dem Stamm oder „Host“ aller unserer Webkomponenten hinzu.
:host {
all: unset;
display: block;
box-sizing: border-box;
text-align: start;
/* ... */
}
Was aber, wenn jemand, der Ihre Webkomponente verwendet, einen berechtigten Grund hat, bestimmte Stile zu ändern? Vielleicht benötigt eine Textzeile aufgrund ihres Kontexts mehr Kontrast oder eine Umrandung muss dicker sein. Wenn keine Stile in Ihre Komponente gelangen können, wie können Sie diese Formatierungsoptionen freischalten?
Hier kommen benutzerdefinierte CSS-Eigenschaften ins Spiel.
Benutzerdefinierte CSS-Properties
Benutzerdefinierte Eigenschaften sind sehr treffend benannt: Es handelt sich um CSS-Eigenschaften, die Sie selbst benennen und denen Sie einen beliebigen Wert zuweisen können. Die einzige Voraussetzung ist, dass Sie ihnen zwei Bindestriche voranstellen. Nachdem Sie die benutzerdefinierte Eigenschaft deklariert haben, kann der Wert in Ihrem CSS mit der Funktion var()
verwendet werden.
:root {
--n-color-accent: rgb(53, 89, 199);
/* ... */
}
.n-color-accent-text {
color: var(--n-color-accent);
}
Alle benutzerdefinierten Properties werden vererbt, was dem typischen Verhalten regulärer CSS-Properties und -Werte entspricht. Jede benutzerdefinierte Property, die auf ein übergeordnetes Element oder das Element selbst angewendet wird, kann als Wert für andere Properties verwendet werden. Wir verwenden benutzerdefinierte Eigenschaften für unsere Design-Tokens, indem wir sie über unser CSS-Framework auf das Stammelement anwenden. Das bedeutet, dass alle Elemente auf der Seite diese Tokenwerte verwenden können, unabhängig davon, ob es sich um eine Webkomponente, eine CSS-Hilfsklasse oder einen Entwickler handelt, der einen Wert aus unserer Liste von Tokens abrufen möchte.
Durch die Möglichkeit, benutzerdefinierte Attribute mithilfe der var()
-Funktion zu übernehmen, können wir das Shadow-DOM unserer Webkomponenten durchdringen und Entwicklern eine genauere Kontrolle beim Gestalten unserer Komponenten ermöglichen.
Benutzerdefinierte Eigenschaften in einer Nord-Webkomponente
Wenn wir eine Komponente für unser Designsystem entwickeln, gehen wir sorgfältig mit dem CSS um. Wir möchten schlanken, aber sehr wartungsfreundlichen Code. Die Design-Tokens, die wir haben, sind als benutzerdefinierte Eigenschaften in unserem Haupt-CSS-Framework für das Root-Element definiert.
:root {
--n-space-m: 16px;
--n-space-l: 24px;
/* ... */
--n-color-background: rgb(255, 255, 255);
--n-color-border: rgb(216, 222, 228);
/* ... */
}
Auf diese Tokenwerte wird dann in unseren Komponenten verwiesen. In einigen Fällen wenden wir den Wert direkt auf die CSS-Eigenschaft an, in anderen Fällen definieren wir eine neue kontextbezogene benutzerdefinierte Eigenschaft und wenden den Wert darauf an.
: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);
/* ... */
}
Außerdem werden einige Werte, die für die Komponente spezifisch sind, aber nicht in unseren Tokens enthalten sind, in eine kontextbezogene benutzerdefinierte Eigenschaft umgewandelt. Benutzerdefinierte Attribute, die im Kontext der Komponente stehen, bieten uns zwei wesentliche Vorteile. Erstens können wir unser CSS „trockener“ gestalten, da dieser Wert auf mehrere Eigenschaften innerhalb der Komponente angewendet werden kann.
.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);
}
Zweitens werden Änderungen am Komponentenstatus und an der Variante sehr übersichtlich. Es muss nur die benutzerdefinierte Eigenschaft geändert werden, um alle diese Eigenschaften zu aktualisieren, wenn Sie beispielsweise einen Hover- oder aktiven Status oder in diesem Fall eine Variante gestalten.
:host([padding="l"]) {
--n-tab-group-padding: var(--n-sp
ace-l);
}
Der größte Vorteil besteht jedoch darin, dass wir durch die Definition dieser kontextbezogenen benutzerdefinierten Eigenschaften für eine Komponente eine Art benutzerdefinierte CSS-API für jede unserer Komponenten erstellen, auf die der Nutzer dieser Komponente zugreifen kann.
<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
Im vorherigen Beispiel sehen Sie eine unserer Webkomponenten, bei der eine kontextbezogene benutzerdefinierte Eigenschaft über einen Selektor geändert wurde. Das Ergebnis dieses Ansatzes ist eine Komponente, die dem Nutzer genügend Flexibilität bei der Gestaltung bietet, während die meisten tatsächlichen Stile weiterhin kontrolliert werden. Außerdem haben wir als Komponentenentwickler die Möglichkeit, die vom Nutzer angewendeten Stile abzufangen. Wenn wir eine dieser Eigenschaften anpassen oder erweitern möchten, können wir das tun, ohne dass der Nutzer seinen Code ändern muss.
Wir finden diesen Ansatz äußerst effektiv, nicht nur für uns als Entwickler unserer Designsystemkomponenten, sondern auch für unser Entwicklungsteam, wenn es diese Komponenten in unseren Produkten verwendet.
Benutzerdefinierte Eigenschaften weiterentwickeln
Zum Zeitpunkt der Erstellung dieses Dokuments werden diese kontextbezogenen benutzerdefinierten Eigenschaften noch nicht in unserer Dokumentation beschrieben. Wir planen jedoch, dies zu tun, damit unser gesamtes Entwicklerteam diese Eigenschaften nachvollziehen und nutzen kann. Unsere Komponenten werden auf npm mit einer Manifestdatei verpackt, die alle Informationen zu den Komponenten enthält. Wir verwenden die Manifestdatei dann als Daten, wenn unsere Dokumentationswebsite bereitgestellt wird. Dies geschieht mit Eleventy und der Funktion für globale Daten. Wir planen, diese kontextbezogenen benutzerdefinierten Properties in diese Manifestdatendatei aufzunehmen.
Ein weiterer Bereich, den wir verbessern möchten, ist die Art und Weise, wie diese kontextbezogenen benutzerdefinierten Eigenschaften Werte übernehmen. Wenn Sie beispielsweise die Farbe von zwei Trennzeichenkomponenten anpassen möchten, müssen Sie derzeit beide Komponenten mit Selektoren ansprechen oder die benutzerdefinierte Eigenschaft direkt auf das Element mit dem Attribut „style“ anwenden. Das mag in Ordnung sein, aber es wäre hilfreicher, wenn der Entwickler diese Stile in einem enthaltenden Element oder sogar auf der Stammebene definieren könnte.
<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>
Der Grund dafür, dass Sie den Wert der benutzerdefinierten Eigenschaft direkt für die Komponente festlegen müssen, ist, dass wir sie über den Komponentenauswahl-Host für dasselbe Element definieren. Die globalen Design-Tokens, die wir direkt in der Komponente verwenden, werden von diesem Problem nicht beeinträchtigt und können sogar auf übergeordneten Elementen abgefangen werden. Wie können wir das Beste aus beiden Welten erhalten?
Private und öffentliche benutzerdefinierte Eigenschaften
Private benutzerdefinierte Eigenschaften wurden von Lea Verou entwickelt. Es handelt sich um eine kontextbezogene „private“ benutzerdefinierte Eigenschaft für die Komponente selbst, die auf eine „öffentliche“ benutzerdefinierte Eigenschaft mit einem Fallback festgelegt ist.
: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);
/* ... */
}
Wenn wir unsere kontextbezogenen benutzerdefinierten Eigenschaften auf diese Weise definieren, können wir weiterhin alle bisherigen Aktionen ausführen, z. B. globale Tokenwerte übernehmen und Werte im gesamten Komponentencode wiederverwenden. Die Komponente übernimmt jedoch auch neue Definitionen dieser Eigenschaft für sich selbst oder ein übergeordnetes Element.
<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>
Auch wenn diese Methode nicht wirklich „privat“ ist, halten wir sie für eine elegante Lösung für ein Problem, das uns Sorgen bereitet hat. Wenn wir die Möglichkeit dazu haben, werden wir das in unseren Komponenten angehen, damit unser Entwicklungsteam mehr Kontrolle über die Verwendung von Komponenten hat und gleichzeitig von den vorhandenen Schutzmaßnahmen profitiert.
Ich hoffe, dieser Einblick in die Verwendung von Webkomponenten mit benutzerdefinierten CSS-Eigenschaften war hilfreich. Lassen Sie uns wissen, was Sie davon halten. Wenn Sie sich entscheiden, eine dieser Methoden in Ihrer eigenen Arbeit anzuwenden, können Sie mich auf Twitter unter @DavidDarnes finden. Sie finden Nordhealth auch auf Twitter unter @NordhealthHQ sowie den Rest meines Teams, das hart daran gearbeitet hat, dieses Designsystem zu entwickeln und die in diesem Artikel erwähnten Funktionen umzusetzen: @Viljamis, @WickyNilliams und @eric_habich.
Hero-Image von Dan Cristian Pădureț