JavaScript Shadow DOM
Das Shadow DOM ermöglicht das Anhängen eines gekapselten DOM-Baums an ein Element, sodass Markup, Styles und Skripte isoliert bleiben. Erfahre mehr über Shadow Roots, Slots und wiederverwendbare Komponenten.
Das Shadow DOM ist ein zentraler Baustein von Web Components und ermöglicht es, einen gekapselten DOM-Baum mit einem isolierten Style-Bereich an ein Element anzuhängen. Dieser Leitfaden behandelt, was das Shadow DOM ist, warum es wichtig ist, wie man offene und geschlossene Shadow Roots erstellt, Styles kapselt, Inhalte mit Slots projiziert und alles in einem wiederverwendbaren Custom Element zusammenfügt.
Was ist Shadow DOM?
Shadow DOM ermöglicht das Anhängen eines separaten, versteckten DOM-Teilbaums an ein Element. Markup und Styles innerhalb dieses Teilbaums sind gekapselt: Sie lecken nicht nach außen, und globale Styles lecken nicht hinein. Das löst eines der ältesten Probleme in der Frontend-Entwicklung — globales CSS und IDs, die sich komponentenübergreifend überschneiden.
Einige Begriffe sind es wert, vorab definiert zu werden:
- Shadow Host — das reguläre Element, an das der Shadow-Baum angehängt wird.
- Shadow Root — der Wurzelknoten des versteckten Baums, der von
attachShadow()zurückgegeben wird. - Shadow Tree — das DOM innerhalb der Shadow Root.
- Light DOM — die gewöhnlichen Kinder des Elements, die in normalem Markup geschrieben sind; diese können über Slots in den Shadow-Baum projiziert werden.
Der Browser selbst verwendet das Shadow DOM intern: Die Steuerelemente eines <video>- oder <input type="range">-Elements befinden sich in einem Shadow-Baum, den man nicht erreichen kann — genau deshalb kollidieren ihre Interna nie mit deinem CSS.
Im folgenden Beispiel teilen sich zwei Elemente die Klasse shadow-box, aber jedes behält sein eigenes Styling, da eines im Hauptdokument und das andere in einer Shadow Root liegt.
<head>
<style>
.shadow-box {
padding: 10px;
border: 1px solid #000;
background-color: lightcoral;
color: white;
}
</style>
</head>
<body>
<div class="shadow-box">This is styled by the main document</div>
<div id="host"></div>
<script>
// Create a shadow root
const hostElement = document.getElementById('host');
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
// Attach shadow DOM content
shadowRoot.innerHTML = `
<style>
.shadow-box {
padding: 10px;
border: 1px solid #000;
background-color: lightblue;
color: black;
}
</style>
<div class="shadow-box">Hello, Shadow DOM!</div>
`;
</script>
</body>In diesem Beispiel gibt es zwei Elemente mit demselben Klassennamen shadow-box. Das erste Element wird durch das CSS des Hauptdokuments gestylt, das zweite durch das CSS des Shadow DOM. Wie zu sehen ist, beeinflussen die im Shadow DOM definierten Styles die Elemente im Hauptdokument nicht und umgekehrt. Dies demonstriert die Kapselung, die das Shadow DOM bietet und es ermöglicht, isolierte und wiederverwendbare Komponenten zu erstellen, ohne sich um Style-Konflikte sorgen zu müssen.
Erstellen einer Shadow Root
Um eine Shadow Root zu erstellen, verwende die Methode attachShadow auf einem Element. Die Shadow Root kann entweder open oder closed sein. Eine open-Shadow Root ist von JavaScript außerhalb des Shadow-Baums zugänglich, eine closed-Shadow Root nicht.
Offene Shadow Root
Eine offene Shadow Root ermöglicht den Zugriff und die Manipulation von externem JavaScript. Im folgenden Beispiel wird der Textinhalt innerhalb der Shadow Root nach ihrer Erstellung manipuliert.
<body>
<div id="open-shadow-host"></div>
<button id="open-shadow-btn">Change Shadow Content</button>
<script>
const openShadowHost = document.getElementById('open-shadow-host');
const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });
openShadowRoot.innerHTML = `
<style>
.shadow-content {
color: blue;
padding: 10px;
border: 1px solid black;
}
</style>
<div class="shadow-content">This is an open shadow root</div>
`;
document.getElementById('open-shadow-btn').addEventListener('click', () => {
openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
});
</script>
</body>In diesem Beispiel wird eine Schaltfläche bereitgestellt, um den Inhalt des Shadow DOM zu ändern. Da die Shadow Root offen ist, können wir auf ihren Inhalt zugreifen und ihn vom Hauptdokument aus manipulieren.
Geschlossene Shadow Root
Eine geschlossene Shadow Root schränkt den Zugriff von externen Skripten ein und bietet bessere Kapselung. Im folgenden Beispiel versuchen wir, den Textinhalt innerhalb der Shadow Root nach ihrer Erstellung zu manipulieren — das ist jedoch nicht möglich, da sie closed ist.
<body>
<div id="closed-shadow-host"></div>
<button id="closed-shadow-btn">Try to Change Shadow Content</button>
<script>
const closedShadowHost = document.getElementById('closed-shadow-host');
const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });
closedShadowRoot.innerHTML = `
<style>
.shadow-content {
color: red;
padding: 10px;
border: 1px solid black;
}
</style>
<div class="shadow-content">This is a closed shadow root</div>
`;
// closedShadowHost.shadowRoot is null for closed roots, so this throws a TypeError
document.getElementById('closed-shadow-btn').addEventListener('click', () => {
try {
closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
} catch (e) {
alert('Cannot access shadow root content from outside!');
}
});
</script>
</body>Hier schlägt der Versuch fehl, weil die Shadow Root geschlossen ist: closedShadowHost.shadowRoot gibt null zurück, sodass null.querySelector(...) einen TypeError wirft und der catch-Block ausgeführt wird. Die von attachShadow({ mode: 'closed' }) zurückgegebene Referenz ist der einzige Weg, diesen Baum zu erreichen — halte sie also privat innerhalb deiner Komponente.
Ein verbreitetes Missverständnis ist, dass closed eine Komponente wirklich sicher macht — das tut es nicht. Es schreckt nur vor dem zufälligen externen Zugriff ab; Code, der die ursprüngliche Root-Referenz hält (oder attachShadow patcht), kann trotzdem hineingreifen. Verwende open, wenn es keinen konkreten Grund gibt, Interna zu verbergen, denn open macht Debugging und Testing wesentlich einfacher.
| Aspekt | mode: 'open' | mode: 'closed' |
|---|---|---|
host.shadowRoot | Gibt die Shadow Root zurück | Gibt null zurück |
| Externer Zugriff | Erlaubt über host.shadowRoot | Nur über die gespeicherte Referenz |
| Typische Verwendung | Die meisten Komponenten, einfaches Debugging | Verbergen von Interna vor Seitenskripten |
| DevTools-Inspektion | Vollständig sichtbar | Sichtbar, aber schwieriger per Skript erreichbar |
Styling im Shadow DOM
Stelle bei der Implementierung des JavaScript Shadow DOM sicher, dass eine ordnungsgemäße Kapselung vorhanden ist, um unbeabsichtigte Styling- oder Skripting-Konflikte zu vermeiden.
Styles, die innerhalb einer Shadow Root definiert sind, beeinflussen keine Elemente außerhalb davon — und umgekehrt. Diese Kapselung ist vorteilhaft für die Erstellung wiederverwendbarer Komponenten.
<head>
<style>
.styled-box {
color: red;
background-color: yellow;
padding: 10px;
border: 1px solid green;
}
</style>
</head>
<body>
<div class="styled-box">This is styled by the main document</div>
<div id="styled-host"></div>
<script>
const styledHost = document.getElementById('styled-host');
const shadowRoot = styledHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
.styled-box {
color: white;
background-color: black;
padding: 10px;
border-radius: 5px;
}
</style>
<div class="styled-box">Styled by Shadow DOM</div>
`;
</script>
</body>In diesem Beispiel gibt es zwei Elemente mit dem Klassennamen styled-box. Das erste Element wird durch das CSS des Hauptdokuments gestylt, das zweite durch das CSS des Shadow DOM. Die im Shadow DOM definierten Styles beeinflussen die Elemente im Hauptdokument nicht, und die im Hauptdokument definierten Styles beeinflussen die Elemente im Shadow DOM nicht. Das demonstriert, wie das Shadow DOM Styles kapselt und so Konflikte zwischen den Styles der Komponente und den globalen Styles verhindert.
Spezielle Selektoren für das Shadow DOM
Kapselung bedeutet keine vollständige Isolation. Drei Selektoren bieten kontrollierte Einstiegspunkte über die Grenze hinweg:
:host— wird innerhalb des Shadow-Baums verwendet, um das Host-Element selbst zu stylen.:host(.active)greift nur dann, wenn der Host diese Klasse trägt.::slotted(selector)— wird innerhalb des Shadow-Baums verwendet, um Light-DOM-Knoten zu stylen, die in einen Slot projiziert wurden. Es können nur die auf oberster Ebene eingefügten Elemente angesprochen werden, nicht ihre Nachkommen.::part(name)— wird im äußeren Dokument verwendet, um ein internes Element zu stylen, das die Komponente explizit mit einempart="name"-Attribut freigibt. Dies ist der empfohlene Weg, um Verbrauchern das Theming einer Komponente zu ermöglichen, ohne in ihre Interna einzugreifen.
<body>
<div id="theme-host">
<span>Projected from the light DOM</span>
</div>
<style>
/* Outer page can only reach parts the component exposes */
#theme-host::part(label) {
text-decoration: underline;
}
</style>
<script>
const host = document.getElementById('theme-host');
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
:host { display: block; padding: 10px; border: 2px solid teal; }
.label { font-weight: bold; color: teal; }
::slotted(span) { color: crimson; }
</style>
<div class="label" part="label">Styled with :host and ::part</div>
<slot></slot>
`;
</script>
</body>Die :host-Regel umrahmt die gesamte Komponente, .label ist intern und privat, ::slotted(span) färbt den projizierten Light-DOM-Text ein, und ::part(label) erlaubt der äußeren Seite, das Label zu unterstreichen, für das sie Gestaltungsrechte erhalten hat. Alles, was nicht als Part freigegeben ist, bleibt von außen unerreichbar.
Slots: Light-DOM-Inhalt im Shadow DOM
Slots ermöglichen es Entwicklern, Light-DOM-Inhalte (reguläre DOM-Inhalte) in ein Shadow DOM zu übergeben, wodurch das Shadow DOM flexibler und wiederverwendbarer wird.
<div id="slot-host">
<span slot="title">Shadow DOM Slot Example</span>
</div>
<script>
const slotHost = document.getElementById('slot-host');
const shadowRoot = slotHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
.container {
border: 1px solid #ccc;
padding: 10px;
}
</style>
<div class="container">
<h1><slot name="title"></slot></h1>
<p>This is a Shadow DOM component with a slot for the title.</p>
</div>
`;
</script>In diesem Beispiel wird das <slot>-Element verwendet, um Inhalte vom Light DOM in das Shadow DOM zu übergeben. Das slot-Attribut des Span-Elements entspricht dem name-Attribut des Slot-Elements im Shadow DOM, wodurch der Inhalt des Spans in das Shadow DOM projiziert wird.
JavaScript-Interaktion mit dem Shadow DOM
Die Interaktion mit dem Shadow DOM über JavaScript erfordert ein Verständnis der Kapselungsgrenzen. Die direkte Manipulation innerhalb der Shadow Root ist unkompliziert, aber externe Interaktion erfordert sorgfältige Handhabung.
Zugriff auf Shadow-DOM-Elemente
Um auf Elemente innerhalb eines Shadow DOM zuzugreifen, verwende die Eigenschaft shadowRoot.
<div id="interactive-host"></div>
<script>
const interactiveHost = document.getElementById('interactive-host');
const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<button id="shadow-btn">Click me</button>
`;
const shadowButton = shadowRoot.querySelector('#shadow-btn');
shadowButton.addEventListener('click', () => {
alert('Button inside Shadow DOM clicked!');
});
</script>In diesem Beispiel wird auf die Schaltfläche im Shadow DOM über querySelector auf der Shadow Root zugegriffen. Da die Shadow Root offen ist, können wir Event-Listener anhängen und Elemente direkt vom Hauptdokument aus manipulieren.
Event-Retargeting
Ereignisse, die aus einem Shadow-Baum nach oben blubbern, werden retargeted: Für Listener im äußeren Dokument zeigt event.target auf den Shadow Host, nicht auf das innere Element, das tatsächlich geklickt wurde. Dadurch bleibt die interne Struktur privat. Innerhalb des Shadow-Baums ist das echte Ziel noch über event.composedPath()[0] oder event.target verfügbar.
<div id="event-host"></div>
<script>
const host = document.getElementById('event-host');
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = '<button id="inner">Click me</button>';
// Listener in the OUTER document
document.addEventListener('click', (e) => {
console.log('Outer target:', e.target.id || e.target.tagName);
console.log('Real target:', e.composedPath()[0].id);
});
</script>Ein Klick auf die Schaltfläche gibt Outer target: event-host aus (auf den Host retargeted), aber Real target: inner aus composedPath(). Beachte, dass Custom Events die Shadow-Grenze nur überschreiten, wenn sie mit { bubbles: true, composed: true } erstellt wurden.
Praktische Beispiele für Shadow DOM
Erstellen einer wiederverwendbaren Web-Komponente
Das Erstellen einer wiederverwendbaren Web-Komponente mit Shadow DOM umfasst das Definieren eines Custom Elements und das Anhängen einer Shadow Root daran.
<body>
<custom-card title="Hello World"></custom-card>
<script>
class CustomCard extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
.card {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.card-title {
font-size: 1.2em;
margin-bottom: 5px;
}
</style>
<div class="card">
<div class="card-title">${this.getAttribute('title')}</div>
<div class="card-content"><slot></slot></div>
</div>
`;
}
}
customElements.define('custom-card', CustomCard);
</script>
</body>In diesem Beispiel wird ein Custom Element <custom-card> mit einem Shadow DOM erstellt. Das Shadow DOM kapselt die Styles und die Struktur der Komponente, wodurch sie wiederverwendbar wird, ohne sich um Style-Konflikte mit dem Hauptdokument sorgen zu müssen. Die Kombination von Shadow DOM mit Custom Elements und dem <template>-Element ist das Standardrezept für produktionsreife Web Components.
Integration mit Frameworks
Shadow DOM lässt sich nahtlos mit modernen JavaScript-Frameworks wie React, Angular und Vue verwenden.
React-Beispiel
In React kann eine Shadow Root wie folgt an ein Container-Element angehängt werden:
<body>
<div id="root"></div>
<!-- React and ReactDOM CDN links -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useRef, useLayoutEffect } = React;
const CustomCard = ({ title, content }) => {
const cardRef = useRef(null);
useLayoutEffect(() => {
if (cardRef.current) {
const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
.card {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.card-title {
font-size: 1.2em;
margin-bottom: 5px;
}
</style>
<div class="card">
<div class="card-title">${title}</div>
<div class="card-content">${content}</div>
</div>
`;
}
}, [title, content]);
return <div ref={cardRef}></div>;
};
const App = () => (
<CustomCard title="Hello World" content="This is content inside the shadow DOM.">
</CustomCard>
);
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
</script>
</body>In diesem Beispiel wird eine React-Komponente CustomCard erstellt, die ein Shadow DOM an ein reguläres Div-Element anhängt. Das Shadow DOM stellt sicher, dass die Styles und die Struktur der Komponente gekapselt sind, was eine nahtlose Integration mit React ermöglicht.
Wann sollte Shadow DOM verwendet werden?
Shadow DOM ist nicht für jede Komponente erforderlich — wäge es daher gegen die Kompromisse ab:
- Verwende es, wenn du ein eigenständiges, wiederverwendbares Widget auslieferst — besonders eines, das auf Seiten eingesetzt wird, deren globales CSS du nicht kontrollierst (Einbettungen, Design-System-Primitive, Widgets von Drittanbietern).
- Verzichte darauf, wenn deine Komponente vollständig in einer App lebt, die Styles bereits kapselt (CSS Modules, Scoped Styles, BEM) und du möchtest, dass globales Theming frei hineinfließt.
- Achte auf diese häufigen Fallstricke:
- Globale Stylesheets und Schriftarten kaskadieren nicht automatisch hinein; deklariere, was du innerhalb der Root benötigst, oder übergib Werte mit CSS Custom Properties (
--my-color), die die Grenze durchdringen. - Formularassoziierte Elemente benötigen zusätzliche Verkabelung (die
ElementInternalsAPI), um an einem umgebenden<form>teilzunehmen. - Server-seitiges Rendering von Shadow-Bäumen erfordert Declarative Shadow DOM (
<template shadowrootmode="open">).
- Globale Stylesheets und Schriftarten kaskadieren nicht automatisch hinein; deklariere, was du innerhalb der Root benötigst, oder übergib Werte mit CSS Custom Properties (
Faustregel: Bevorzuge mode: 'open' und biete Theming-Hooks mit ::part() und CSS Custom Properties an. Verwende closed nur, wenn das Verbergen von Interna eine echte Anforderung ist.
Fazit
Die Beherrschung des Shadow DOM ist wesentlich für die moderne Webentwicklung und bietet leistungsstarke Kapselung und Wiederverwendbarkeit. Durch das Verstehen und Anwenden der hier erläuterten Konzepte und Beispiele kannst du robuste, isolierte Komponenten erstellen, die die Wartbarkeit und Skalierbarkeit deiner Webanwendungen verbessern.
Dieser umfassende Leitfaden soll als solide Grundlage dienen, um das Shadow DOM in deinen Projekten zu erkunden und zu nutzen. Ob du einfache Widgets oder komplexe Anwendungen erstellst — das Shadow DOM bietet die Kapselung und Flexibilität, die notwendig ist, damit deine Komponenten isoliert und handhabbar bleiben.