W3docs

Shadow DOM Styling

Web-Komponenten mit Shadow DOM stylen: :host, :host(), :host-context(), ::slotted(), CSS-Custom-Properties und adoptedStyleSheets.

Das Shadow DOM gibt einer Komponente ihren eigenen privaten DOM-Baum und eigene private Styles. Diese Seite konzentriert sich auf den Styling-Teil: wie CSS innerhalb eines Shadow Roots gekapselt ist, die speziellen Selektoren für den Host und den geslotteten Inhalt (:host, :host(), :host-context(), ::slotted()), wie externe CSS eine Komponente gezielt anpassen kann (Custom Properties) und die beiden Wege, ein Stylesheet einzubinden (<style> vs. adoptedStyleSheets).

Falls der Shadow DOM neu für dich ist, lies zuerst JavaScript Shadow DOM und schau dir Web Components an, um ein größeres Bild davon zu bekommen, wo Shadow Roots hineinpassen.

Warum Styles gekapselt sind

Das Kernversprechen des Shadow DOM ist eine beidseitige Style-Grenze:

  • Externes CSS dringt nicht ein. Eine seitenweite Regel wie p { color: red } hat keinen Einfluss auf ein <p> innerhalb eines Shadow Roots. Das macht Komponenten sicher, die man auf beliebigen Seiten einsetzen kann.
  • Internes CSS dringt nicht nach außen. Styles in einem Shadow Root gelten nur innerhalb dieses Roots, sodass kurze, generische Selektoren (button, p, .title) verwendet werden können, ohne Konflikte mit der Host-Seite befürchten zu müssen.

Dies unterscheidet sich vom regulären Modell für Styles und Klassen, bei dem jeder Selektor in einem einzigen globalen Scope konkurriert. Innerhalb eines Shadow Roots ist Scope der Standard.

Einen Shadow Root erstellen

Zunächst wird ein Shadow Root an ein Host-Element angehängt. Alles, was darin platziert wird — Markup und CSS — ist gekapselt.

<body>
  <div id="my-element"></div>
    <script>
      // Creating Shadow DOM
      const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });
    
      // Styling Shadow DOM
      shadowRoot.innerHTML = `
        <p>A simple shadow root content.</p>
      `;
    </script>
</body>

Hier wird ein Shadow Root mit der Methode attachShadow() angehängt, wobei mode auf 'open' gesetzt wird. Das ermöglicht es, den Root später über element.shadowRoot auszulesen. ('closed' verbirgt ihn vor externen Skripten, bietet aber keine echte Sicherheit.)

Scoped Styles mit <style> hinzufügen

Der einfachste Weg, einen Shadow Root zu stylen, ist ein <style>-Element darin zu platzieren. Diese Regeln gelten nur innerhalb des Roots — und die Regeln der Seite bleiben draußen.

<div id="my-element">
  <!-- Shadow DOM content -->
</div>

<script>
  const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      /* Scoped styles */
      :host {
        display: block;
        border: 2px solid #333;
        padding: 10px;
      }

      p {
        color: blue;
      }
    </style>

    <p>This paragraph is styled within the Shadow DOM.</p>
  `;
</script>

Die Regel p { color: blue } färbt nur den Absatz innerhalb dieses Roots — ein <p> an anderer Stelle auf der Seite bleibt unberührt. Die :host-Regel (unten) stylt das Host-Element selbst.

Den Host ansprechen: :host, :host(), :host-context()

Ein Shadow Root kann sein Host-Element nicht mit einem normalen Selektor auswählen, da der Host außerhalb des Roots lebt. Drei Pseudo-Klassen schließen diese Lücke:

SelektorTrifft zu aufVerwendungszweck
:hostDas Host-Element, immerBasisstile für die Komponente (display, padding, Box).
:host(<selector>)Den Host nur wenn er auf <selector> passtVarianten und Zustände, die durch Attribute/Klassen/Pseudo-Klassen gesteuert werden, z. B. :host([disabled]), :host(:hover).
:host-context(<selector>)Den Host, wenn ein Vorfahre auf <selector> passtAn Kontext anpassen, z. B. :host-context(.dark-theme).
<div class="dark-theme">
  <fancy-box disabled>Boxed content</fancy-box>
</div>

<script>
  class FancyBox extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host {
            display: block;
            padding: 12px;
            border: 2px solid #007bff;
          }
          /* Variant: applies only when the host has [disabled] */
          :host([disabled]) {
            opacity: 0.5;
            pointer-events: none;
          }
          /* Context: applies when any ancestor has .dark-theme */
          :host-context(.dark-theme) {
            background: #1e1e1e;
            color: #fff;
          }
        </style>
        <slot></slot>
      `;
    }
  }
  customElements.define('fancy-box', FancyBox);
</script>

Da das Host-Element mit [disabled] beginnt und innerhalb von .dark-theme sitzt, gelten alle drei Regeln: Es wird dunkel, abgeblendet und nicht interaktiv dargestellt.

Warnung

:host-context() hat eine eingeschränkte Browser-Unterstützung (zum Zeitpunkt der Erstellung kein Firefox). Bevorzuge eine CSS-Custom-Property oder ein explizites Attribut am Host, wenn breite Kompatibilität erforderlich ist.

Geslotteten Inhalt stylen mit ::slotted()

Inhalt, den der Nutzer in eine Komponente übergibt, lebt im Light DOM und wird über ein <slot> gerendert. Solcher Inhalt gehört weiterhin zur Seite, daher haben die eigenen Styles der Seite Vorrang — aber man kann ihn innerhalb des Shadow Roots trotzdem mit ::slotted() erreichen.

Eine wichtige Einschränkung: ::slotted() trifft nur die obersten geslotteten Knoten, nicht deren Nachkommen. ::slotted(span) funktioniert; ::slotted(div span) nicht.

<body>
<script>
  class CustomButton extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: 'open' });

      shadowRoot.innerHTML = `
        <style>
          :host {
            display: inline-block;
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
          }

          :host(:hover) {
            background-color: #0056b3;
          }

          button {
            font-weight: bold;
            border: none;
            background: none;
            color: inherit;
            cursor: inherit;
            padding: 0;
          }

          /* Styling slotted content */
          ::slotted(span) {
            font-style: italic;
            text-decoration: underline;
          }
        </style>

        <button>
          <slot></slot>
        </button>
      `;
    }
  }

  customElements.define('custom-button', CustomButton);
</script>

<!-- Test custom-button with slotted content -->
<custom-button id="my-button">Click <span>here</span></custom-button>
</body>

Hier zielt ::slotted(span) auf das <span>, das als Slot-Inhalt übergeben wird, und macht es kursiv und unterstrichen, während der umgebende Text "Click" unverändert bleibt.

Der Seite erlauben, eine Komponente anzupassen: CSS-Custom-Properties

Kapselung ist großartig, kann sich aber wie eine Mauer anfühlen: Die Host-Seite kann nicht hinein, um einen Button umzufärben. Der vorgesehene Ausweg sind CSS-Custom-Properties (Variablen) — sie sind das Einzige, das die Shadow-Grenze tatsächlich durchdringt. Die Komponente liest eine Variable mit var() aus und liefert einen Fallback-Wert; die Seite setzt diese Variable von außen.

<style>
  /* The page customizes the component from outside the boundary */
  theme-button {
    --btn-bg: #28a745;
    --btn-bg-hover: #1e7e34;
  }
</style>

<theme-button>Save</theme-button>

<script>
  class ThemeButton extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host {
            /* var(--name, fallback): fallback is used if the page sets nothing */
            background: var(--btn-bg, #007bff);
            color: #fff;
            padding: 8px 16px;
            display: inline-block;
            cursor: pointer;
          }
          :host(:hover) {
            background: var(--btn-bg-hover, #0056b3);
          }
        </style>
        <slot></slot>
      `;
    }
  }
  customElements.define('theme-button', ThemeButton);
</script>

Der Button erscheint grün, weil die Seite --btn-bg gesetzt hat. Werden diese beiden Deklarationen entfernt, fällt er auf Blau zurück (#007bff). Das ist der sauberste Weg, eine Theming-API bereitzustellen und gleichzeitig die internen Details der Komponente privat zu halten.

<style> vs. adoptedStyleSheets

Ein <style>-Tag in das innerHTML jeder Instanz zu setzen funktioniert, dupliziert aber den CSS-Text für jede Komponente auf der Seite und zwingt den Browser, ihn jedes Mal neu zu parsen. Für Komponenten, die viele Male erstellt werden, empfiehlt es sich, ein einziges geparstes CSSStyleSheet über Roots hinweg mit adoptedStyleSheets zu teilen.

<my-badge>New</my-badge>
<my-badge>Beta</my-badge>

<script>
  // Parsed once, reused by every instance
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
    :host {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 999px;
      background: #007bff;
      color: #fff;
      font-size: 12px;
    }
  `);

  class MyBadge extends HTMLElement {
    constructor() {
      super();
      const root = this.attachShadow({ mode: 'open' });
      root.adoptedStyleSheets = [sheet]; // adopt the shared sheet
      root.innerHTML = `<slot></slot>`;
    }
  }
  customElements.define('my-badge', MyBadge);
</script>

Wann was verwenden:

  • <style> innerhalb des Roots — am einfachsten, kein zusätzlicher Code, gut für einmalige Komponenten oder kleine Demos.
  • adoptedStyleSheets — bevorzugt, wenn dieselbe Komponente viele Male erscheint: Ein gemeinsames, konstruierbares Stylesheet bedeutet weniger Speicherverbrauch und schnellere Instanziierung. Das Sheet kann auch zur Laufzeit aktualisiert werden (sheet.replaceSync(...)), und jeder adoptierende Root spiegelt die Änderung sofort wider.

Fazit

Shadow DOM Styling basiert auf einigen wenigen Konzepten: Styles sind in beide Richtungen abgegrenzt, der Host wird mit :host / :host() / :host-context() angesprochen, projizierbarer Inhalt mit ::slotted(), Theming wird über CSS-Custom-Properties bereitgestellt, und CSS wird entweder inline mit <style> oder effizient mit adoptedStyleSheets eingebunden. Zusammen ermöglichen sie Komponenten, die überall korrekt aussehen und dennoch nach eigenen Bedingungen anpassbar bleiben.

Um tiefer einzutauchen, sieh dir Shadow DOM Slots & Composition an, um zu erfahren, wie geslotteter Inhalt zusammengestellt wird, und Web Components für die Kombination von Shadow Roots mit Custom Elements und Templates.

Übungen

Übung
Welches Feature des Shadow DOM ermöglicht es Entwicklern, in Custom Elements projizierten Inhalt zu stylen?
Welches Feature des Shadow DOM ermöglicht es Entwicklern, in Custom Elements projizierten Inhalt zu stylen?
Was this page helpful?