Ich muss mich bei Jeremy Keith und seinem wunderbar einsichtigen Artikel von Ende letzten Jahres bedanken, der mich in das Konzept der HTML Web Components eingeführt hat. Das war für mich der „Aha-Moment“.
Wenn man ein bestehendes Markup in ein benutzerdefiniertes Element (Custom Element) einhüllt und dann mit JavaScript ein neues Verhalten hinzufügt, macht man technisch gesehen nichts, was man nicht auch vorher mit DOM-Traversierung und Ereignisbehandlung hätte tun können. Aber mit einer Web Component ist es weniger fragil. Es ist portabel. Es folgt dem Single-Responsibility-Prinzip. Es macht nur eine Sache, aber es macht sie gut.
Bis dahin war ich fälschlicherweise davon ausgegangen, dass alle Web Components ausschließlich auf JavaScript in Verbindung mit dem eher erschreckend klingenden Shadow DOM angewiesen sind. Obwohl man Web Components tatsächlich so erstellen kann, gibt es noch einen anderen Weg. Vielleicht einen besseren? Besonders wenn man sich, wie ich, für Progressive Enhancement einsetzt. HTML Web Components sind schließlich einfach nur HTML.
Obwohl es den Rahmen dessen sprengt, was wir hier besprechen, hat Andy Bell vor kurzem einen Bericht verfasst, der seine (exzellente) Sichtweise darauf bietet, was Progressive Enhancement eigentlich bedeutet.
Schauen wir uns drei spezifische Beispiele an, die die meiner Meinung nach wichtigsten Merkmale von HTML Web Components zeigen – CSS-Stilkapselung und Möglichkeiten für Progressive Enhancement –, ohne dass sie von Haus aus zwingend auf JavaScript angewiesen sind. Wir werden definitiv JavaScript verwenden, aber die Komponenten sollten auch ohne funktionieren.
Die Beispiele finden sich alle in meiner Web UI Boilerplate Komponenten-Bibliothek (erstellt mit Storybook), zusammen mit dem zugehörigen Quellcode auf GitHub.
Beispiel 1: <webui-disclosure>

Mir gefällt sehr, wie Chris Ferdinandi das Erstellen einer Web Component von Grund auf lehrt, wobei er ein Disclosure-Muster (Einblenden/Ausblenden) als Beispiel verwendet. Dieses erste Beispiel erweitert seine Demo.
Beginnen wir mit dem Bürger erster Klasse: HTML. Web Components ermöglichen es uns, benutzerdefinierte Elemente mit eigener Namensgebung zu erstellen, wie in diesem Beispiel mit dem Tag <webui-disclosure>. Wir verwenden es, um einen <button> (zum Ein-/Ausblenden eines Textblocks) und ein <div> (das den Text-<p> enthält) aufzunehmen.
<webui-disclosure
data-bind-escape-key
data-bind-click-outside
>
<button
type="button"
class="button button--text"
data-trigger
hidden
>
Show / Hide
</button>
<div data-content>
<p>Content to be shown/hidden.</p>
</div>
</webui-disclosure>
Wenn JavaScript deaktiviert ist oder nicht ausgeführt wird (aus diversen Gründen), ist die Schaltfläche standardmäßig ausgeblendet – dank des hidden-Attributs – und der Inhalt im Div wird einfach standardmäßig angezeigt.
Schön. Das ist ein sehr einfaches Beispiel für Progressive Enhancement in der Praxis. Ein Besucher kann den Inhalt mit oder ohne <button> sehen.
Ich erwähnte, dass dieses Beispiel die ursprüngliche Demo von Chris Ferdinandi erweitert. Der Hauptunterschied besteht darin, dass man das Element entweder durch Drücken der ESC-Taste oder durch Klicken außerhalb des Elements schließen kann. Dafür sind die beiden [data-attribute] am <webui-disclosure>-Tag gedacht.
Wir beginnen damit, das Custom Element zu definieren, damit der Browser weiß, was er mit unserem erfundenen Tag-Namen tun soll.
customElements.define('webui-disclosure', WebUIDisclosure);
Custom Elements müssen einen Namen mit einem Bindestrich (dashed-ident) haben, wie zum Beispiel <my-pizza>, aber wie Jim Neilsen über Scott Jehl anmerkt, bedeutet das nicht zwangsläufig, dass der Bindestrich zwischen zwei Wörtern stehen muss.
Normalerweise bevorzuge ich TypeScript für das Schreiben von JavaScript, um dumme Fehler zu vermeiden und ein gewisses Maß an „defensiver“ Programmierung zu erzwingen. Aber der Einfachheit halber sieht die Struktur des ES-Moduls der Web Component in einfachem JavaScript so aus:
default class WebUIDisclosure extends HTMLElement {
constructor() {
super();
this.trigger = this.querySelector('[data-trigger]');
this.content = this.querySelector('[data-content]');
this.bindEscapeKey = this.hasAttribute('data-bind-escape-key');
this.bindClickOutside = this.hasAttribute('data-bind-click-outside');
if (!this.trigger || !this.content) return;
this.setupA11y();
this.trigger?.addEventListener('click', this);
}
setupA11y() {
// Add ARIA props/state to button.
}
// Handle constructor() event listeners.
handleEvent(e) {
// 1. Toggle visibility of content.
// 2. Toggle ARIA expanded state on button.
}
// Handle event listeners which are not part of this Web Component.
connectedCallback() {
document.addEventListener('keyup', (e) => {
// Handle ESC key.
});
document.addEventListener('click', (e) => {
// Handle clicking outside.
});
}
disconnectedCallback() {
// Remove event listeners.
}
}
Fragen Sie sich wegen dieser Event-Listener? Der erste wird in der constructor()-Funktion definiert, während der Rest in der connectedCallback()-Funktion steht. Hawk Ticehurst erklärt die Gründe viel eleganter, als ich es könnte.
Dieses JavaScript ist nicht erforderlich, damit die Web Component „funktioniert“, aber es fügt einige nette Funktionalitäten sowie Barrierefreiheits-Aspekte hinzu, um das Progressive Enhancement zu unterstützen, das es dem <button> ermöglicht, den Inhalt ein- und auszublenden. Zum Beispiel injiziert JavaScript die entsprechenden Attribute aria-expanded und aria-controls, damit Nutzer von Screenreadern den Zweck der Schaltfläche verstehen.
Das ist der Teil zum Progressive Enhancement in diesem Beispiel.
Der Einfachheit halber habe ich für diese Komponente kein zusätzliches CSS geschrieben. Das Styling, das Sie sehen, wird einfach aus dem bestehenden globalen Bereich oder aus Komponenten-Styles (z. B. Typografie und Buttons) übernommen.
Das nächste Beispiel hat jedoch etwas zusätzliches, gekapseltes CSS.
Beispiel 2: <webui-tabs>
Das erste Beispiel zeigt die Vorteile von HTML Web Components für das Progressive Enhancement auf. Ein weiterer Vorteil ist die Kapselung von CSS-Stilen. Das ist eine schicke Art zu sagen, dass das CSS nicht aus der Komponente „herausleckt“. Die Stile sind rein auf die Web Component beschränkt und kollidieren nicht mit anderen Stilen auf der Seite.
Kommen wir zu einem zweiten Beispiel, das diesmal die Stilkapselung von Web Components demonstriert und zeigt, wie sie Progressive Enhancement in der Benutzererfahrung unterstützen. Wir verwenden eine Tab-Komponente, um Inhalte in „Panels“ zu organisieren, die eingeblendet werden, wenn der entsprechende Tab geklickt wird – genau so etwas, wie man es in vielen Komponenten-Bibliotheken findet.

Beginnen wir mit der HTML-Struktur:
<webui-tabs>
<div data-tablist>
<a href="#tab1" data-tab>Tab 1</a>
<a href="#tab2" data-tab>Tab 2</a>
<a href="#tab3" data-tab>Tab 3</a>
</div>
<div id="tab1" data-tabpanel>
<p>1 - Lorem ipsum dolor sit amet consectetur.</p>
</div>
<div id="tab2" data-tabpanel>
<p>2 - Lorem ipsum dolor sit amet consectetur.</p>
</div>
<div id="tab3" data-tabpanel>
<p>3 - Lorem ipsum dolor sit amet consectetur.</p>
</div>
</webui-tabs>
Sie verstehen das Prinzip: drei als Tabs gestaltete Links, die beim Anklicken ein Tab-Panel mit Inhalt öffnen. Beachten Sie, dass jedes [data-tab] in der Tab-Liste auf einen Ankerlink zielt, der einer Tab-Panel-ID entspricht, z. B. #tab1, #tab2 usw.
Wir schauen uns zuerst die Stilkapselung an, da wir darauf im letzten Beispiel nicht eingegangen sind. Nehmen wir an, das CSS ist so organisiert:
webui-tabs {
[data-tablist] {
/* Default styles without JavaScript */
}
[data-tab] {
/* Default styles without JavaScript */
}
[role='tablist'] {
/* Style role added by JavaScript */
}
[role='tab'] {
/* Style role added by JavaScript */
}
[role='tabpanel'] {
/* Style role added by JavaScript */
}
}
Sehen Sie, was hier passiert? Wir haben zwei Stilregeln – [data-tablist] und [data-tab] –, die die Standardstile der Web Component enthalten. Mit anderen Worten: Diese Stile sind vorhanden, egal ob JavaScript geladen wird oder nicht. Die anderen drei Stilregeln sind Selektoren, die nur dann in die Komponente injiziert werden, wenn JavaScript aktiviert ist und unterstützt wird. Auf diese Weise werden die letzten drei Stilregeln nur angewendet, wenn JavaScript das role-Attribut auf diese Elemente im HTML setzt. Damit bieten wir bereits einen Hauch von Progressive Enhancement, indem wir Stile nur dann festlegen, wenn JavaScript benötigt wird.
Alle diese Stile sind vollständig auf die <webui-tabs>-Web-Component gekapselt oder beschränkt. Es gibt kein „Auslaufen“, das die Stile anderer Web Components oder andere Elemente im globalen Bereich der Seite beeinflussen würde. Wir können sogar auf Klassennamen, komplexe Selektoren und Methoden wie BEM verzichten und stattdessen einfache Nachfahren-Selektoren für die Kindelemente der Komponente verwenden, was uns erlaubt, Stile deklarativer für semantische Elemente zu schreiben.
Kurz: „Light“ DOM vs. Shadow DOM
Für die meisten Webprojekte bevorzuge ich es generell, CSS (einschließlich der Web Component Sass-Partials) in einer einzigen CSS-Datei zu bündeln, damit die Standardstile der Komponente im globalen Bereich verfügbar sind, auch wenn das JavaScript nicht ausgeführt wird.
Es ist jedoch möglich, ein Stylesheet via JavaScript zu importieren, das nur von dieser Web Component verwendet wird, sofern JavaScript verfügbar ist:
import styles from './styles.css';
class WebUITabs extends HTMLElement {
constructor() {
super();
this.adoptedStyleSheets = [styles];
}
}
customElements.define('webui-tabs', WebUITabs);
Alternativ könnten wir stattdessen ein <style>-Tag injizieren, das die Stile der Komponente enthält:
class WebUITabs extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }); // Required for JavaScript access
this.shadowRoot.innerHTML = `
<style> <!-- styles go here --> </style>
// etc.
`;
}
}
customElements.define('webui-tabs', WebUITabs);
Welche Methode Sie auch wählen, diese Stile sind direkt auf die Web Component beschränkt, was verhindert, dass Komponentenstile nach außen dringen, aber erlaubt, dass globale Stile geerbt werden.
Betrachten Sie nun dieses einfache Beispiel. Alles, was wir zwischen den öffnenden und schließenden Tags der Komponente schreiben, wird als Teil des „Light“ DOM betrachtet.
<my-web-component>
<!-- This is Light DOM -->
<div>
<p>Some content... styles are inherited from the global scope</p>
</div>
----------- Shadow DOM Boundary -------------
| <!-- Anything injected by JavaScript --> |
---------------------------------------------
</my-web-component>
Dave Rupert hat einen exzellenten Artikel geschrieben, der sehr anschaulich zeigt, wie externe Stile in der Lage sind, den Shadow DOM zu „durchbrechen“ und ein Element im Light DOM auszuwählen. Beachten Sie, wie das <button>-Element, das zwischen den Tags des Custom Elements geschrieben wurde, die Stile des button-Selektors im globalen CSS erhält, während der via JavaScript injizierte <button> unberührt bleibt.
Wenn wir den Shadow-DOM-<button> stylen wollen, müssten wir das mit internen Stilen tun, wie in den obigen Beispielen für den Import eines Stylesheets oder das Einfügen eines Inline-<style>-Blocks.
Das bedeutet nicht, dass alle CSS-Eigenschaften vom Shadow DOM blockiert werden. Tatsächlich listet Dave 37 Eigenschaften auf, die Web Components erben (inherit), meist im Bereich der Text-, Listen- und Tabellenformatierung.
Tab-Komponente mit JavaScript progressiv verbessern
Obwohl es in diesem zweiten Beispiel eher um Stilkapselung geht, ist es dennoch eine gute Gelegenheit, das Progressive Enhancement zu sehen, das wir bei Web Components fast geschenkt bekommen. Schauen wir uns nun das JavaScript an, um zu sehen, wie wir Progressive Enhancement unterstützen können. Der vollständige Code ist recht lang, daher habe ich ihn etwas gekürzt, um die Punkte klarer zu machen.
default class WebUITabs extends HTMLElement {
constructor() {
super();
this.tablist = this.querySelector('[data-tablist]');
this.tabpanels = this.querySelectorAll('[data-tabpanel]');
this.tabTriggers = this.querySelectorAll('[data-tab]');
if (
!this.tablist ||
this.tabpanels.length === 0 ||
this.tabTriggers.length === 0
) return;
this.createTabs();
this.tabTriggers.forEach((tabTrigger, index) => {
tabTrigger.addEventListener('click', (e) => {
this.bindClickEvent(e);
});
tabTrigger.addEventListener('keydown', (e) => {
this.bindKeyboardEvent(e, index);
});
});
}
createTabs() {
// 1. Hide all tabpanels initially.
// 2. Add ARIA props/state to tabs & tabpanels.
}
bindClickEvent(e) {
e.preventDefault();
// Show clicked tab and update ARIA props/state.
}
bindKeyboardEvent(e, index) {
e.preventDefault();
// Handle keyboard ARROW/HOME/END keys.
}
}
customElements.define('webui-tabs', WebUITabs);
Das JavaScript injiziert ARIA-Rollen, Zustände und Eigenschaften in die Tabs und Inhaltsblöcke für Screenreader-Nutzer sowie zusätzliche Tastaturbindungen, damit wir mit der Tastatur zwischen den Tabs navigieren können; zum Beispiel ist die TAB-Taste für den Zugriff auf den aktiven Tab der Komponente und alle fokussierbaren Inhalte innerhalb des aktiven tabpanel reserviert, und die Tabs können mit den PFEILTASTEN durchlaufen werden. Wenn JavaScript also nicht geladen wird, ist die Standarderfahrung immer noch barrierefrei, da die Tabs weiterhin als Ankerlinks zu ihren jeweiligen Panels führen und diese Panels natürlich vertikal übereinander gestapelt werden.
Und wenn JavaScript aktiviert ist und unterstützt wird? Dann erhalten wir eine verbesserte Erfahrung, ergänzt durch aktualisierte Barrierefreiheits-Aspekte.
Beispiel 3: <webui-ajax-loader>

Dieses letzte Beispiel unterscheidet sich von den vorangegangenen dadurch, dass es vollständig durch JavaScript generiert wird und den Shadow DOM nutzt. Dies liegt daran, dass es nur verwendet wird, um einen „Ladezustand“ für Ajax-Anfragen anzuzeigen, und daher nur benötigt wird, wenn JavaScript aktiviert ist.
Das HTML-Markup besteht lediglich aus den öffnenden und schließenden Komponenten-Tags:
<webui-ajax-loader></webui-ajax-loader>
Die vereinfachte JavaScript-Struktur:
default class WebUIAjaxLoader extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<svg role="img" part="svg">
<title>loading</title>
<circle cx="50" cy="50" r="47" />
</svg>
`;
}
}
customElements.define('webui-ajax-loader',WebUIAjaxLoader);
Beachten Sie sofort, dass alles zwischen den <webui-ajax-loader>-Tags mit JavaScript injiziert wird, was bedeutet, dass sich alles im Shadow DOM befindet, gekapselt von anderen Skripten und Stilen, die nicht direkt mit der Komponente gebündelt sind.
Beachten Sie aber auch das part-Attribut, das auf dem <svg>-Element gesetzt ist. Hier schauen wir genauer hin:
<svg role="img" part="svg">
<!-- etc. -->
</svg>
Das ist ein weiterer Weg, um das Custom Element zu stylen: Named Parts. Jetzt können wir dieses SVG von außerhalb des Template-Literals stylen, das wir zum Erstellen des Elements verwendet haben. Dafür gibt es den Pseudo-Selektor ::part:
webui-ajax-loader::part(svg) {
// Shadow DOM styles for the SVG...
}
Und hier ist etwas Cooles: Dieser Selektor kann auf CSS Custom Properties zugreifen, egal ob sie global oder lokal auf das Element beschränkt sind.
webui-ajax-loader {
--fill: orangered;
}
webui-ajax-loader::part(svg) {
fill: var(--fill);
}
Was das Progressive Enhancement betrifft, liefert JavaScript das gesamte HTML. Das bedeutet, dass der Loader nur gerendert wird, wenn JavaScript aktiviert ist und unterstützt wird. Wenn das der Fall ist, wird das SVG hinzugefügt, komplett mit einem barrierefreien Titel und allem Drum und Dran.
Zusammenfassung
Das war's mit den Beispielen! Ich hoffe, dass Sie jetzt dieselbe Erkenntnis haben wie ich beim Lesen von Jeremy Keiths Beitrag: HTML Web Components sind ein HTML-first Feature.
Natürlich spielt JavaScript eine große Rolle, aber nur so groß wie nötig. Benötigen Sie mehr Kapselung? Möchten Sie etwas UX-Extras hinzufügen, wenn der Browser eines Besuchers dies unterstützt? Dafür ist JavaScript da, und das macht HTML Web Components zu einer so großartigen Ergänzung der Webplattform – sie verlassen sich auf Vanilla-Websprachen, um das zu tun, wofür sie von Anfang an konzipiert wurden, ohne sich zu sehr auf das eine oder andere zu stützen.
Ich möchte mehr von dir lernen, mach weiter so mit der tollen Arbeit
Vorsicht damit, so viel in den Constructor zu packen. Du wirst viele Fehler erhalten, wenn du diese Web Component programmatisch erstellst. Diese Kindelemente sind zum Abfragen möglicherweise noch nicht da.
Danke für die Warnung… aber ich kodiere bereits defensiv, so dass ich abbreche, wenn die Kindelemente nicht existieren
https://github.com/basher/Web-UI-Boilerplate/blob/master/ui/src/javascript/web-components/webui-disclosure.ts#L15
https://github.com/basher/Web-UI-Boilerplate/blob/master/ui/src/javascript/web-components/webui-tabs.ts#L13
Denk auch daran, dass die Beispiele in diesem Artikel (naja, 2 davon) HTML Web Components sind, der HTML-Teil wird also nicht programmatisch generiert.
Wollte die gleiche Warnung aussprechen.
JS kann verwendet werden, um das Light DOM zu manipulieren, einschließlich Attributwerten oder dem dynamischen Hinzufügen/Entfernen von Tabs.
Events „bubblen“, daher ist es besser, Event-Listener für innere Light-DOM-Elemente auf der Web Component selbst einzurichten.
Anstatt „defensiv zu programmieren“, könntest du es auch einfach so bauen, dass es funktioniert.
https://dev.to/dannyengelman/web-component-developers-do-not-connect-with-the-connectedcallback-yet-4jo7
Kann eine WebComponent funktional implementiert werden, oder bin ich an JS-Frameworks gebunden?
Chris Ferdinandi hat einige exzellente Informationen zu Callbacks bei Custom Events, die einen Blick wert sind.