So verwendet Nordhealth benutzerdefinierte Eigenschaften in Webkomponenten

Die Vorteile der Verwendung benutzerdefinierter Eigenschaften in Designsystemen und Komponentenbibliotheken.

David Darnes
David Darnes

Ich heiße Dave und bin Senior Front-End Developer bei Nordhealth. Ich arbeite am Design und an der Entwicklung unseres Designsystems Nord, das auch die Erstellung von Webkomponenten für unsere Komponentenbibliothek umfasst. Ich möchte Ihnen zeigen, wie wir die Probleme beim Gestalten 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

Für die Erstellung unserer Webkomponenten verwenden wir Lit. Diese Bibliothek bietet eine Menge Boilerplate-Code wie z. B. Status, Bereichsstile, Vorlagen und mehr. Lit ist nicht nur einfach, sondern basiert auch auf nativen JavaScript APIs. Dies bedeutet, dass wir ein schlankes Code-Bundle bereitstellen können, das die bereits vorhandenen 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);
Eine mit „Lit.“ geschriebene Webkomponente

Das Schönste an Webkomponenten ist jedoch, dass sie mit fast jedem vorhandenen JavaScript-Framework oder sogar mit keinem Framework kompatibel sind. Sobald auf der Seite auf das Haupt-JavaScript-Paket verwiesen wird, ist die Verwendung einer Webkomponente der Verwendung eines nativen HTML-Elements sehr ähnlich. Das einzig wahre Hinweis darauf, dass es sich nicht um ein natives HTML-Element handelt, ist der einheitliche Bindestrich innerhalb der Tags. Mit diesem Standard wird dem Browser mitgeteilt, dass es sich um eine Webkomponente handelt.


// TODO: DevSite - Code sample removed as it used inline event handlers
Die oben erstellte Webkomponente auf einer Seite verwenden

Kapselung des Schatten-DOM-Stils

Ähnlich wie native HTML-Elemente haben auch Webkomponenten ein Shadow DOM. Das Shadow-DOM ist eine versteckte Knotenstruktur innerhalb eines Elements. Die beste Möglichkeit, dies zu visualisieren, ist, den Web Inspector zu öffnen und die Option „Show Shadow DOM Tree“ (Shadow-DOM-Baum anzeigen) zu aktivieren. Sehen Sie sich anschließend ein natives Eingabeelement im Inspector an. Sie haben nun die Möglichkeit, diese Eingabe zu öffnen und alle darin enthaltenen Elemente zu sehen. Sie können das sogar mit einer unserer Webkomponenten ausprobieren. Unter unsere benutzerdefinierte Eingabekomponente sehen Sie das Shadow DOM.

Das in den Entwicklertools geprüfte Schatten-DOM
Beispiel für das Shadow DOM in einem normalen Texteingabeelement und in unserer Nord-Eingabe-Webkomponente

Einer der Vor- oder Nachteile von Shadow DOM ist die Stilkapselung. Wenn Sie CSS in Ihrer Web Component schreiben, dürfen diese Stile nicht auslaufen und sich auf die Hauptseite oder andere Elemente auswirken. Sie sind vollständig in der Komponente enthalten. Darüber hinaus darf CSS, das für die Hauptseite oder eine übergeordnete Web Component geschrieben wurde, nicht in Ihre Web Component eindringen.

Diese Datenkapselung von Stilen ist ein Vorteil in unserer Komponentenbibliothek. So können wir gewährleisten, dass, wenn jemand eine unserer Komponenten verwendet, diese wie beabsichtigt aussieht, unabhängig von den Stilen, die auf die übergeordnete Seite angewendet werden. Und um sicherzugehen, fügen wir all: unset; dem Stammverzeichnis bzw. „Host“ aller unserer Webkomponenten hinzu.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Der Boilerplate-Code einer Komponente wird auf den Schattenstamm oder den Hostselektor angewendet.

Was ist jedoch, wenn jemand, der Ihre Webkomponente verwendet, einen legitimen Grund hat, bestimmte Stile zu ändern? Vielleicht gibt es eine Textzeile, die aufgrund ihres Kontexts mehr Kontrast benötigt, oder muss ein Rahmen dicker sein? Wie können Sie diese Stiloptionen freischalten, wenn keine Stile in Ihre Komponente eindringen können?

Hier kommen benutzerdefinierte CSS-Eigenschaften ins Spiel.

Benutzerdefinierte CSS-Eigenschaften

Benutzerdefinierte Eigenschaften sind sehr passend benannt. Es sind CSS-Eigenschaften, die Sie sich selbst benennen und jeden erforderlichen Wert anwenden können. Die einzige Anforderung besteht darin, dass Sie sie mit zwei Bindestrichen voranstellen müssen. Nachdem Sie die benutzerdefinierte Eigenschaft deklariert haben, kann der Wert mit der var()-Funktion in Ihrem CSS-Code verwendet werden.


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Beispiel aus unserem CSS-Framework, wie ein Designtoken als benutzerdefinierte Eigenschaft in einer Hilfsklasse verwendet wird

Bei der Vererbung werden alle benutzerdefinierten Eigenschaften übernommen, 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 nutzen benutzerdefinierte Eigenschaften für unsere Designtokens in großem Umfang, indem wir sie über unser CSS Framework auf das Stammelement anwenden. Das bedeutet, dass alle Elemente auf der Seite diese Tokenwerte verwenden können, sei es eine Webkomponente, eine CSS-Hilfsklasse oder ein Entwickler, der einen Wert aus unserer Liste von Tokens heraussuchen möchte.

Die Möglichkeit, benutzerdefinierte Eigenschaften mithilfe der var()-Funktion zu übernehmen, bietet die Möglichkeit, das Shadow DOM unserer Webkomponenten zu durchdringen. So haben Entwickler mehr Kontrolle bei der Gestaltung unserer Komponenten.

Benutzerdefinierte Eigenschaften in einer Nord Web-Komponente

Wann immer wir eine Komponente für unser Designsystem entwickeln, verfolgen wir bei der Verwendung des CSS-Codes eine durchdachte Herangehensweise – wir streben einen schlanken, aber sehr gut zu verwaltenden Code an. Die Designtokens, die wir definiert haben, sind in unserem Haupt-CSS-Framework auf dem Stammelement als benutzerdefinierte Eigenschaften definiert.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Benutzerdefinierte CSS-Eigenschaften werden für die Stammauswahl definiert.

Auf diese Tokenwerte wird dann in unseren Komponenten verwiesen. In einigen Fällen wenden wir den Wert direkt auf die CSS-Eigenschaft an, bei anderen hingegen 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);
  /* ... */
}
Benutzerdefinierte Eigenschaften, die für den Schattenstamm der Komponente definiert und dann in den Komponentenstilen verwendet werden. Auch benutzerdefinierte Eigenschaften aus der Liste der Designtokens werden verwendet.

Außerdem werden einige Werte abstrahiert, die für die Komponente, aber nicht in unseren Tokens spezifisch sind, und wandeln sie in eine kontextbezogene benutzerdefinierte Eigenschaft um. Benutzerdefinierte Eigenschaften, die für die Komponente kontextabhängig sind, bieten uns zwei wesentliche Vorteile. Erstens bedeutet dies, dass wir mit unserem CSS etwas „trockener“ sein können, 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);
}
Die kontextbezogene benutzerdefinierte Eigenschaft für das Padding von Tabgruppen, die an mehreren Stellen im Komponentencode verwendet wird

Zum anderen werden Änderungen am Komponentenstatus und an den Variationen extrem sauber – es ist nur die benutzerdefinierte Eigenschaft, die geändert werden muss, um alle diese Eigenschaften zu aktualisieren, wenn Sie z. B. einen Hover- oder aktiven Status oder, wie in diesem Fall, eine Variation gestalten.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Eine Variante der Tab-Komponente, bei der der Abstand mithilfe einer einzelnen Aktualisierung der benutzerdefinierten Eigenschaft geändert wird, statt mehrere Anpassungen vorzunehmen

Der wichtigste Vorteil besteht jedoch darin, dass wir bei der Definition dieser kontextbezogenen benutzerdefinierten Eigenschaften für eine Komponente eine Art benutzerdefinierte CSS API für jede unserer Komponenten erstellen, auf die der Nutzer der jeweiligen Komponente zugreifen kann.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Sie verwenden die Tabgruppenkomponente auf der Seite und erhöhen die benutzerdefinierte Eigenschaft für den Abstand.

Das Beispiel oben zeigt eine unserer Webkomponenten mit einer kontextbezogenen benutzerdefinierten Eigenschaft, die über einen Selektor geändert wurde. Das Ergebnis dieses Ansatzes ist eine Komponente, die dem Nutzer genügend Gestaltungsflexibilität bietet und gleichzeitig einen Großteil der tatsächlichen Stile unter Kontrolle hält. 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 sehr wirkungsvoll, nicht nur für uns als Entwickler unserer Designsystemkomponenten, sondern auch für unser Entwicklungsteam, wenn es diese Komponenten in unseren Produkten verwendet.

Weitere Möglichkeiten mit benutzerdefinierten Eigenschaften

Zum Zeitpunkt der Erstellung dieses Dokuments veröffentlichen wir diese kontextbezogenen benutzerdefinierten Properties noch nicht in unserer Dokumentation. Wir möchten jedoch, dass unser Entwicklungsteam sie verstehen und nutzen kann. Unsere Komponenten werden auf npm in einer Manifestdatei verpackt, die alle wichtigen Informationen über sie enthält. Anschließend verarbeiten wir die Manifestdatei als Daten, wenn unsere Dokumentationswebsite bereitgestellt wird. Dies erfolgt mithilfe von Eleventy und dessen Funktion „Global Data“. Wir planen, diese kontextbezogenen benutzerdefinierten Eigenschaften in diese Manifest-Datendatei aufzunehmen.

Ebenfalls zu verbessern ist die Art und Weise, wie diese kontextbezogenen benutzerdefinierten Eigenschaften Werte übernehmen. Wenn Sie beispielsweise die Farbe von zwei Trennlinienkomponenten anpassen möchten, müssten Sie das Targeting auf beide Komponenten explizit mithilfe von Selektoren vornehmen oder die benutzerdefinierte Eigenschaft direkt auf das Element mit dem Stilattribut anwenden. Das mag in Ordnung erscheinen, es wäre jedoch hilfreicher, wenn der Entwickler diese Stile in einem beinhaltenden 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>
Zwei Instanzen der Trennlinie, die zwei verschiedene Farbbehandlungen erfordern. Eines ist in einem Abschnitt verschachtelt, den wir für einen spezifischeren Selektor verwenden können, aber wir müssen die Trennlinie speziell ausrichten.

Sie müssen den Wert für die benutzerdefinierte Eigenschaft direkt in der Komponente festlegen, weil wir ihn über die Hostauswahl für die Komponente für dasselbe Element definieren. Die globalen Design-Tokens, die wir direkt in der Komponente verwenden, werden direkt weitergeleitet, sind von diesem Problem nicht betroffen und können sogar von übergeordneten Elementen abgefangen werden. Wie können wir das Beste aus beiden Welten machen?

Private und öffentliche benutzerdefinierte Eigenschaften

Private benutzerdefinierte Eigenschaften wurden von Lea Verou zusammengestellt. Dabei handelt es sich um eine kontextabhängige, "private" benutzerdefinierte Eigenschaft auf der 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);
  /* ... */
}
Der CSS-Code der Webkomponente mit den kontextbezogenen benutzerdefinierten Eigenschaften, die so angepasst wurden, dass das interne CSS auf einer privaten benutzerdefinierten Eigenschaft basiert, die auf eine öffentliche benutzerdefinierte Eigenschaft mit einem Fallback festgelegt wurde.

Wenn wir unsere kontextbezogenen benutzerdefinierten Eigenschaften auf diese Weise definieren, können wir weiterhin alles tun, was wir zuvor getan haben, wie die Übernahme globaler Tokenwerte und die Wiederverwendung von Werten in unserem Komponentencode. Die Komponente übernimmt aber 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>
Wie gesagt die beiden Trennlinien, aber dieses Mal kann die Trennlinie neu eingefärbt werden, indem der Abschnittsauswahl die kontextabhängige benutzerdefinierte Eigenschaft der Trennlinie hinzugefügt wird. Diese wird von der Trennlinie übernommen, was einen saubereren und flexibleren Code ergibt.

Auch wenn man sagen kann, dass diese Methode nicht wirklich „privat“ sei, halten wir sie dennoch für eine elegante Lösung für ein Problem, um das wir uns Sorgen gemacht haben. Wenn die Möglichkeit besteht, werden wir dies in unseren Komponenten angehen, damit unser Entwicklungsteam mehr Kontrolle über die Komponentennutzung hat und gleichzeitig von den vorhandenen Sicherheitsmaßnahmen profitieren kann.

Ich hoffe, dieser Einblick in die Verwendung von Webkomponenten mit benutzerdefinierten CSS-Eigenschaften war hilfreich. Teilen Sie uns mit, was Sie davon halten. Wenn Sie sich entscheiden, eine dieser Methoden in Ihrer eigenen Arbeit zu verwenden, teilen Sie uns dies auf Twitter @DavidDarnes mit. Sie finden Nordhealth @NordhealthHQ auch auf Twitter sowie den Rest meines Teams, das hart daran gearbeitet hat, dieses Designsystem zusammenzustellen und die in diesem Artikel erwähnten Funktionen auszuführen: @Viljamis, @WickyNilliams und @eric_habich.

Hero-Image von Dan Cristian Pădureț