So verwendet Nordhealth benutzerdefinierte Eigenschaften in Webkomponenten

Die Vorteile der Verwendung benutzerdefinierter Eigenschaften in Designsystemen und Komponentenbibliotheken.

David Darnes
David Darnes

Ich bin Dave und Senior-Frontend-Entwickler bei Nordhealth. Ich bin am Design und der Entwicklung unseres Designsystems Nord beteiligt. Dazu gehört auch die Entwicklung von Webkomponenten für unsere Komponentenbibliothek. Ich möchte Ihnen gern erläutern, wie wir die Probleme beim Gestalten von Webkomponenten mithilfe von benutzerdefinierten CSS-Eigenschaften gelöst haben und wie Sie benutzerdefinierte Eigenschaften in Designsystemen und Komponentenbibliotheken nutzen konnten.

So entwickeln wir Webkomponenten

Zur Erstellung unserer Webkomponenten verwenden wir Lit. Diese Bibliothek enthält umfangreiches Codebausteine wie Status, Bereichsstile, Vorlagen und mehr. Er ist nicht nur schlank, sondern baut auch auf nativen JavaScript-APIs auf. So können wir ein schlankes Codepaket bereitstellen, das die bereits vorhandenen Browserfunktionen 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 Beste an Web-Komponenten ist jedoch, dass sie mit fast jedem vorhandenen JavaScript-Framework oder sogar ohne Framework funktionieren. Sobald auf das Haupt-JavaScript-Paket auf der Seite verwiesen wird, ähnelt die Verwendung einer Webkomponente der Verwendung eines nativen HTML-Elements. Das einzige wirkliche Zeichen dafür, dass es sich nicht um ein natives HTML-Element handelt, ist der einheitliche Bindestrich innerhalb der Tags, der dem Browser anzeigt, dass es sich um eine Webkomponente handelt.


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

Shadow-DOM-Stil-Kapselung

Ähnlich wie native HTML-Elemente ein Shadow DOM haben, tun es auch Webkomponenten. Shadow DOM ist eine verborgene Baumstruktur von Knoten innerhalb eines Elements. Am besten lässt sich dies visualisieren, indem Sie den Web Inspector öffnen und die Option „Shadow DOM-Baum anzeigen“ aktivieren. Sehen Sie sich dann ein natives Eingabeelement im Inspector an. Sie können es jetzt öffnen und alle darin enthaltenen Elemente sehen. Sie können dies sogar mit einer unserer Webkomponenten ausprobieren. Sehen Sie sich unsere benutzerdefinierte Eingabekomponente an, um ihr Shadow DOM zu sehen.

Das in den Entwicklertools geprüfte Shadow DOM.
Beispiel für das Shadow DOM in einem regulären Texteingabeelement und in unserer Nord-Eingabe-Webkomponente.

Einer der Vorteile (oder Nachteile, je nach Sichtweise) von Shadow DOM ist die Stilkapselung. Wenn Sie CSS in Ihrer Webanwendung schreiben, können diese Stile nicht austreten und sich auf die Hauptseite oder andere Elemente auswirken. 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 eindringen.

Diese Kapselung der Stile ist ein Vorteil in unserer Komponentenbibliothek. Es gibt uns eher die Garantie, dass eine unserer Komponenten, wenn jemand verwendet wird, wie beabsichtigt aussieht, unabhängig von den Stilen, die auf der übergeordneten Seite angewendet wurden. Zur Sicherheit fügen wir all: unset; auch der Wurzel oder dem „Host“ aller Webkomponenten hinzu.


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

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

Hier kommen benutzerdefinierte CSS-Properties ins Spiel.

Benutzerdefinierte CSS-Properties

Benutzerdefinierte Eigenschaften sind sehr gut benannt. Dabei handelt es sich um CSS-Eigenschaften, die Sie selbst benennen und alle erforderlichen Werte anwenden können. Die einzige Voraussetzung ist, dass Sie ihnen zwei Bindestriche voranstellen. Nachdem Sie die benutzerdefinierte Property deklariert haben, kann der Wert in Ihrem CSS mithilfe der Funktion var() verwendet werden.


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
Beispiel aus unserem CSS-Framework eines Designtokens als benutzerdefiniertes Attribut, das in einer Hilfsklasse verwendet wird

Bei der Übernahme werden alle benutzerdefinierten Properties übernommen. Das entspricht dem üblichen Verhalten regulärer CSS-Properties und -Werte. Jede benutzerdefinierte Property, die auf ein übergeordnetes Element oder das Element selbst angewendet wird, kann als Wert für andere Properties verwendet werden. Wir nutzen benutzerdefinierte Eigenschaften in großem Umfang für unsere Designtokens, 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 übernehmen möchte.

Mithilfe der Funktion var() können benutzerdefinierte Eigenschaften übernommen werden, um das Shadow-DOM unserer Webkomponenten zu durchdringen und Entwicklern eine detailliertere Kontrolle beim Stylen unserer Komponenten zu ermöglichen.

Benutzerdefinierte Eigenschaften in einer Nord-Webkomponente

Immer, wenn wir eine Komponente für unser Designsystem entwickeln, gehen wir beim CSS durchdacht vor. Unser Ziel ist es, schlanker, aber gut verwaltbarer Code zu verwenden. Die Designtokens, die wir haben, sind in unserem Haupt-CSS-Framework für das 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-Properties, die im Stammselektor definiert werden.

Auf diese Tokenwerte wird dann in unseren Komponenten verwiesen. In einigen Fällen wird der Wert direkt auf die CSS-Eigenschaft angewendet. In anderen Fällen wird eine neue kontextbezogene benutzerdefinierte Property definiert und der Wert darauf angewendet.


: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 Properties, die im Schatten-Root der Komponente definiert und dann in den Komponentenstilen verwendet werden. Benutzerdefinierte Eigenschaften aus der Liste der Designtokens werden ebenfalls verwendet.

Außerdem abstrahieren wir einige Werte, die für die Komponente spezifisch sind, aber nicht in unseren Tokens enthalten sind, und wandeln sie in eine benutzerdefinierte Kontexteigenschaft um. Benutzerdefinierte Eigenschaften, die für die Komponente kontextabhängig sind, bieten uns zwei wesentliche Vorteile. Erstens: Wir können unser CSS schlanker gestalten, da dieser Wert auf mehrere Properties 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 benutzerdefinierte Kontexteigenschaft für das Tab-Gruppenabstand wird an mehreren Stellen im Komponentencode verwendet.

Und zweitens lassen sich Änderungen am Komponentenstatus und an Variationen sehr klar erkennen. Nur die benutzerdefinierte Eigenschaft muss geändert werden, um all diese Eigenschaften zu aktualisieren, z. B. wenn Sie einen Hover- oder aktiven Status oder in diesem Fall eine Variation gestalten.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Eine Variante der Tabkomponente, bei der der Innenrand mit einer einzigen Aktualisierung der benutzerdefinierten Property geändert wird.

Der größte Vorteil besteht jedoch darin, dass wir durch die Definition dieser benutzerdefinierten Kontexteigenschaften für eine Komponente eine Art benutzerdefinierte CSS-API für jede unserer Komponenten erstellen, die vom Nutzer dieser Komponente genutzt werden kann.


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

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Die Tab-Gruppenkomponente auf der Seite verwenden und die benutzerdefinierte Eigenschaft „Abstand“ auf eine größere Größe aktualisieren

Das obige Beispiel zeigt eine unserer Webkomponenten mit einer kontextbezogenen benutzerdefinierten Property, die über einen Selektor geändert wurde. Das Ergebnis dieses gesamten Ansatzes ist eine Komponente, die dem Nutzer ausreichend Flexibilität beim Styling bietet und gleichzeitig die meisten der tatsächlichen Stile im Auge behält. 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, ist das möglich, 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 diese Komponenten in unseren Produkten verwenden.

Benutzerdefinierte Eigenschaften weiter optimieren

Zum Zeitpunkt der Erstellung dieses Dokuments werden diese kontextbezogenen benutzerdefinierten Properties nicht in unserer Dokumentation offengelegt. Wir möchten jedoch dafür sorgen, dass unser Entwicklungsteam diese Properties verstehen und nutzen kann. Unsere Komponenten sind in npm mit einer Manifestdatei gepackt, die alle notwendigen Informationen über die Komponenten enthält. Die Manifestdatei wird dann als Daten verarbeitet, wenn unsere Dokumentationswebsite bereitgestellt wird. Dies geschieht mit Eleventy und der zugehörigen Funktion Global Data. Wir planen, diese kontextbezogenen benutzerdefinierten Eigenschaften in diese Manifest-Datendatei aufzunehmen.

Ein weiterer Bereich, den wir verbessern möchten, ist die Art und Weise, wie diese kontextbezogenen benutzerdefinierten Eigenschaften Werte erben. Wenn Sie derzeit beispielsweise die Farbe von zwei Trennlinien anpassen möchten, müssen Sie die Ausrichtung auf beide Komponenten speziell mit Selektoren vornehmen oder die benutzerdefinierte Eigenschaft direkt auf das Element mit dem Stilattribut anwenden. Das mag in Ordnung erscheinen, aber es wäre hilfreicher, wenn der Entwickler diese Stile für ein enthaltenes 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>
Zwei Instanzen unserer Fahrbahntrennungskomponente, die in zwei verschiedenen Farben bearbeitet werden müssen. Eine davon ist in einem Abschnitt verschachtelt, den wir für eine spezifischere Auswahl verwenden können, aber wir müssen die Trennlinie speziell ansteuern.

Der Wert für die benutzerdefinierte Property muss direkt auf der Komponente festgelegt werden, da wir sie über die Auswahl für den Komponentenhost auf demselben Element definieren. Die globalen Designtokens, 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 bekommen?

Private und öffentliche benutzerdefinierte Eigenschaften

Private benutzerdefinierte Eigenschaften wurden von Lea Verou zusammengestellt. Dabei handelt es sich um eine kontextbezogene "private" benutzerdefinierte Eigenschaft auf der Komponente selbst, die jedoch auf eine "öffentliche" benutzerdefinierte Eigenschaft mit einem Fallback festgelegt wurde.



: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);
  /* ... */
}
CSS der Webkomponente mit der Trennlinie mit den kontextbezogenen benutzerdefinierten Eigenschaften angepasst, sodass das interne CSS auf einer privaten benutzerdefinierten Property basiert, die auf eine öffentliche benutzerdefinierte Property mit einem Fallback festgelegt wurde

Wenn wir unsere kontextbezogenen benutzerdefinierten Eigenschaften auf diese Weise definieren, können wir immer noch alle Dinge tun, die wir zuvor getan haben, 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 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>
Noch einmal die beiden Trennlinien. Jetzt kann sie jedoch neu eingefärbt werden, indem die kontextbezogene benutzerdefinierte Eigenschaft der Trennlinie zur Abschnittsauswahl hinzugefügt wird. Die Trennlinie übernimmt diese und ergibt einen saubereren und flexibleren Code.

Auch wenn es möglicherweise argumentiert, dass diese Methode nicht wirklich „privat“ ist, denken wir, dass dies eine ziemlich elegante Lösung für ein Problem ist, über das wir uns Sorgen machen. Bei Gelegenheit werden wir dies in unseren Komponenten angehen, damit unser Entwicklungsteam mehr Kontrolle über die Komponentennutzung hat und gleichzeitig von unseren Sicherheitsvorkehrungen profitiert.

Wir hoffen, diese Informationen zur Verwendung von Webkomponenten mit benutzerdefinierten CSS-Eigenschaften waren für Sie hilfreich. Lasst uns wissen, was ihr davon haltet. Wenn ihr eine dieser Methoden in eurer eigenen Arbeit verwenden möchtet, könnt ihr 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 zusammenzustellen und die in diesem Artikel erwähnten Funktionen umzusetzen: @Viljamis, @WickyNilliams und @eric_habich.

Hero-Image von Dan Cristian Pădureț