Die Vorteile der Verwendung benutzerdefinierter Eigenschaften in Designsystemen und Komponentenbibliotheken.
Ich bin Dave und Senior Frontend-Entwickler bei Nordhealth. Ich arbeite am Design und an der Entwicklung unseres Designsystems Nord, einschließlich der Erstellung von Webkomponenten für unsere Komponentenbibliothek. Ich möchte Ihnen zeigen, wie wir die Probleme beim Gestalten von Webkomponenten mithilfe von benutzerdefinierten CSS-Eigenschaften gelöst haben, und einige der anderen Vorteile der Verwendung von benutzerdefinierten Eigenschaften in Designsystemen und Komponentenbibliotheken erläutern.
So erstellen wir Webkomponenten
Zum Erstellen unserer Webkomponenten verwenden wir Lit, eine Bibliothek, die viel Boilerplate-Code wie Status, bereichsbezogene Stile, Vorlagen und mehr bietet. Lit ist nicht nur leichtgewichtig, sondern basiert auch auf nativen JavaScript APIs. Das bedeutet, dass wir ein schlankes Code-Bundle bereitstellen können, das die bereits im Browser vorhandenen Funktionen 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 Attraktivste an Webkomponenten ist jedoch, dass sie mit fast jedem vorhandenen JavaScript-Framework oder sogar ganz ohne Framework funktionieren. Sobald das Haupt-JavaScript-Paket auf der Seite referenziert ist, ist die Verwendung einer Webkomponente sehr ähnlich wie die Verwendung eines nativen HTML-Elements. Das einzige wirkliche Anzeichen dafür, dass es sich nicht um ein natives HTML-Element handelt, ist der Bindestrich innerhalb der Tags. Dieser ist ein Standard, der dem Browser signalisiert, dass es sich um eine Webkomponente handelt.
Shadow-DOM-Stilkapselung
Ähnlich wie native HTML-Elemente haben auch Webkomponenten ein Shadow DOM. Das Shadow-DOM ist ein ausgeblendeter Knotenbaum innerhalb eines Elements. Am besten lässt sich das veranschaulichen, indem Sie den Web Inspector öffnen und die Option „Shadow DOM-Baum anzeigen“ aktivieren. Sehen Sie sich danach ein natives Eingabeelement im Inspektor an. Sie haben jetzt die Möglichkeit, dieses Eingabeelement zu öffnen und alle darin enthaltenen Elemente zu sehen. Sie können das sogar mit einer unserer Webkomponenten ausprobieren. Untersuchen Sie dazu unsere benutzerdefinierte Eingabekomponente, um das Shadow DOM zu sehen.
Einer der Vorteile (oder Nachteile, je nach Sichtweise) von Shadow DOM ist die Stil-Kapselung. 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 sicher sein, dass unsere Komponenten unabhängig von den Stilen der übergeordneten Seite wie beabsichtigt aussehen. Um ganz sicherzugehen, fügen wir all: unset; zum 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 legitimen Grund hat, bestimmte Stile zu ändern? Vielleicht muss eine Textzeile aufgrund ihres Kontexts mehr Kontrast haben oder ein Rahmen muss dicker sein? Wenn keine Stile in Ihre Komponente gelangen können, wie können Sie dann diese Stiloptionen freischalten?
Hier kommen benutzerdefinierte CSS-Eigenschaften ins Spiel.
Benutzerdefinierte CSS-Eigenschaften
Benutzerdefinierte Eigenschaften sind 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 Ihre 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 Eigenschaften werden vererbt, was dem typischen Verhalten regulärer CSS-Eigenschaften und -Werte entspricht. Jede benutzerdefinierte Eigenschaft, die auf ein übergeordnetes Element oder das Element selbst angewendet wird, kann als Wert für andere Eigenschaften verwendet werden. Wir verwenden benutzerdefinierte Eigenschaften für unsere Design-Tokens, indem wir sie mithilfe unseres CSS-Frameworks auf das Stammelement anwenden. Das bedeutet, dass alle Elemente auf der Seite diese Token-Werte verwenden können, sei es eine Webkomponente, eine CSS-Hilfsklasse oder ein Entwickler, der einen Wert aus unserer Token-Liste auswählen möchte.
Durch die Möglichkeit, benutzerdefinierte Eigenschaften mit der Funktion var() zu übernehmen, können wir das Shadow DOM unserer Webkomponenten durchbrechen und Entwicklern eine genauere Kontrolle beim Gestalten unserer Komponenten ermöglichen.
Benutzerdefinierte Eigenschaften in einer Nord-Webkomponente
Bei der Entwicklung einer Komponente für unser Designsystem gehen wir beim CSS-Code sehr sorgfältig vor. Wir streben einen schlanken, aber sehr wartungsfreundlichen Code an. Die Design-Tokens sind als benutzerdefinierte Eigenschaften im Haupt-CSS-Framework im Stammelement definiert.
:root {
--n-space-m: 16px;
--n-space-l: 24px;
/* ... */
--n-color-background: rgb(255, 255, 255);
--n-color-border: rgb(216, 222, 228);
/* ... */
}
Diese Tokenwerte werden dann in unseren Komponenten referenziert. In einigen Fällen wenden wir den Wert direkt auf die CSS-Property 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);
/* ... */
}
Wir abstrahieren auch einige Werte, die für die Komponente spezifisch sind, aber nicht in unseren Tokens enthalten sind, und wandeln sie in eine kontextbezogene benutzerdefinierte Eigenschaft um. Benutzerdefinierte Eigenschaften, die sich auf die Komponente beziehen, bieten uns zwei wichtige 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 den Varianten 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-space-l);
}
Der größte Vorteil ist jedoch, dass wir durch die Definition dieser kontextbezogenen benutzerdefinierten Eigenschaften für eine Komponente eine Art benutzerdefinierte CSS-API für jede unserer Komponenten erstellen, die vom Nutzer dieser Komponente verwendet werden kann.
<nord-tab-group label="Title">
<!-- ... -->
</nord-tab-group>
<style>
nord-tab-group {
--n-tab-group-padding: var(--n-space-xl);
}
</style>
Das vorherige Beispiel zeigt eine unserer Webkomponenten mit einer kontextbezogenen benutzerdefinierten Eigenschaft, die mithilfe eines Selektors geändert wurde. Das Ergebnis dieses gesamten Ansatzes ist eine Komponente, die dem Nutzer genügend Flexibilität beim Styling bietet, während die meisten der tatsächlichen Stile im Zaum gehalten werden. Außerdem können wir als Komponentenentwickler die vom Nutzer angewendeten Stile abfangen. 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 extrem leistungsstark, nicht nur für uns als Ersteller der Komponenten unseres Designsystems, sondern auch für unser Entwicklungsteam, wenn es diese Komponenten in unseren Produkten verwendet.
Benutzerdefinierte Eigenschaften weiter anpassen
Zum Zeitpunkt der Erstellung dieses Dokuments werden diese kontextbezogenen benutzerdefinierten Eigenschaften in unserer Dokumentation nicht offengelegt. Wir planen jedoch, dies zu tun, damit unser gesamtes Entwicklungsteam diese Eigenschaften verstehen und nutzen kann. Unsere Komponenten sind auf npm mit einer Manifestdatei verpackt, die alle Informationen zu ihnen enthält. Die Manifestdatei wird dann als Daten verwendet, wenn unsere Dokumentationswebsite bereitgestellt wird. Dies geschieht mit Eleventy und der Global Data-Funktion. Wir planen, diese kontextbezogenen benutzerdefinierten Eigenschaften in diese Manifest-Datendatei aufzunehmen.
Ein weiterer Bereich, den wir verbessern möchten, ist die Vererbung von Werten durch diese kontextbezogenen benutzerdefinierten Eigenschaften. Wenn Sie beispielsweise die Farbe von zwei Trennlinienkomponenten anpassen möchten, müssen Sie beide Komponenten gezielt mit Selektoren ansprechen oder die benutzerdefinierte Eigenschaft mit dem Stilattribut direkt auf das Element anwenden. Das mag in Ordnung sein, aber es wäre hilfreicher, wenn der Entwickler diese Stile für ein übergeordnetes Element oder sogar auf 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, warum Sie den Wert der benutzerdefinierten Eigenschaft direkt für die Komponente festlegen müssen, ist, dass wir sie mit dem Host-Selektor der Komponente für dasselbe Element definieren. Die globalen Design-Tokens, die wir direkt in der Komponente verwenden, werden direkt weitergegeben, sind von diesem Problem nicht betroffen und können sogar von übergeordneten Elementen abgefangen werden. Wie können wir das Beste aus beiden Welten herausholen?
Private und öffentliche benutzerdefinierte Eigenschaften
Private benutzerdefinierte Eigenschaften wurden von Lea Verou zusammengestellt. Dabei handelt es sich um eine kontextbezogene „private“ benutzerdefinierte Eigenschaft für die Komponente selbst, die jedoch 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 alles tun, was wir zuvor getan haben, z. B. globale Tokenwerte übernehmen und Werte im gesamten Komponentencode wiederverwenden. Die Komponente übernimmt aber auch neue Definitionen dieser Eigenschaft für sich selbst oder für 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>
Man könnte zwar argumentieren, dass diese Methode nicht wirklich „privat“ ist, aber wir halten sie dennoch für eine recht elegante Lösung für ein Problem, das uns Sorgen bereitet hat. Wenn sich die Gelegenheit bietet, werden wir dieses Problem 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. Lasst uns wissen, was ihr denkt. Wenn ihr eine dieser Methoden in eurer eigenen Arbeit verwendet, könnt ihr mich auf Twitter unter @DavidDarnes finden. Sie finden Nordhealth auch auf Twitter unter @NordhealthHQ. Dort finden Sie auch die anderen Mitglieder meines Teams, die hart daran gearbeitet haben, dieses Designsystem zusammenzustellen und die in diesem Dokument erwähnten Funktionen zu implementieren: @Viljamis, @WickyNilliams und @eric_habich.