Claude Code Web Components: Custom Elements, Shadow DOM und Browser-APIs mit KI entwickeln

Web Components sind einer der am häufigsten missverstandenen Teile des modernen Webs. Sie sind kein Framework — sie sind eine Browserplattform-Spezifikation. Custom Elements, Shadow DOM, HTML Templates und Slots sind seit Jahren in allen modernen Browsern nativ verfügbar, ohne Build-Step, ohne Abhängigkeiten, ohne Bundle-Größe. Trotzdem greifen die meisten Teams sofort zu React oder Vue, weil das Erstellen eigener Web Components umständlich wirkt.

Claude Code ändert das. Nicht weil es die Browser-API einfacher macht — die API bleibt wie sie ist. Sondern weil Claude Code die gesamte Spezifikation kennt, die richtigen Lifecycle-Methoden zur richtigen Zeit einsetzt, Shadow DOM korrekt kapselt und Interoperabilitätsfallen von vornherein vermeidet. Dieser Artikel zeigt, wie das in der Praxis funktioniert.

Claude Code Mastery — KI-gestützte Webentwicklung auf Deutsch

Web Components sind ein Kapitel. Der Kurs zeigt, wie du Claude Code für den gesamten Entwicklungsworkflow einsetzt — von der Komponentenarchitektur bis zum produktiven Deployment. Einmalig bezahlt, kein Abo.

Zum Kurs — ab €29 → Basis €29 · Pro €49 · Einmalzahlung · Kein Abo

1. Web Components Grundlagen: Custom Elements, Shadow DOM, HTML Templates und Slots

Web Components bestehen aus vier Browser-APIs, die einzeln nutzbar sind, zusammen aber ein vollständiges Komponentenmodell ergeben:

Der entscheidende Vorteil gegenüber React-Komponenten: Web Components sind echte HTML-Elemente. Sie funktionieren in jedem Framework, in plain HTML, in serverseitig gerendertem Markup — ohne dass der Konsument weiß, wie die Komponente intern aufgebaut ist. Sie sind die einzige Komponentenform, die wirklich framework-agnostisch ist.

Claude Code kennt alle vier APIs vollständig. Wenn du ein Custom Element beschreibst, wählt Claude Code automatisch die richtigen Primitiven: Shadow DOM wenn CSS-Kapselung gewünscht ist, Templates wenn die Struktur wiederverwendet wird, Slots wenn externer Inhalt eingefügt werden soll. Du musst nicht selbst entscheiden, was davon nötig ist.

Browser-Support 2026: Custom Elements v1, Shadow DOM v1 und HTML Templates sind in Chrome, Firefox, Safari und Edge vollständig unterstützt. Kein Polyfill nötig für moderne Browser-Targets. Für ältere Browser-Unterstützung gibt es den @webcomponents/webcomponentsjs Polyfill.

Was Claude Code beim ersten Prompt liefert

Beschreibe eine Komponente in natürlicher Sprache, und Claude Code entscheidet, welche der vier Browser-APIs nötig sind — und setzt sie korrekt zusammen:

claude "Erstelle eine Web Component 'tooltip-element' die beim Hover
eines beliebigen Inhalts einen Tooltip anzeigt. Shadow DOM fuer
Kapselung, Slot fuer den Inhalt, CSS Custom Properties fuer Farben."

Was Claude Code daraus macht: ein vollständiges Custom Element mit Shadow DOM, korrektem <template>-Einsatz, einem Default-Slot für den Trigger-Inhalt, einem named Slot für den Tooltip-Text, CSS Custom Properties als öffentliche API und den richtigen Lifecycle-Callbacks. Nicht ein Grundgerüst — eine einsatzfähige Komponente.

2. Custom Elements API: define, connectedCallback, attributeChangedCallback und der Lifecycle

Das Custom Elements API dreht sich um eine Klasse, die HTMLElement erweitert, und eine Registrierung im customElements-Registry. Die Lifecycle-Methoden sind der eigentliche Kern — und der Teil, den Entwickler am häufigsten falsch einsetzen.

class CounterElement extends HTMLElement {
  static get observedAttributes() {
    return ['value', 'step'];
  }

  constructor() {
    super();
    // Shadow DOM im Constructor attachen, nicht in connectedCallback
    this._shadow = this.attachShadow({ mode: 'open' });
    this._count = 0;
    this._step = 1;
  }

  connectedCallback() {
    // DOM-Manipulation und Event-Listener hier
    this._render();
    this._shadow.querySelector('button')
      .addEventListener('click', () => this._increment());
  }

  disconnectedCallback() {
    // Event-Listener hier aufraumen
    this._shadow.querySelector('button')
      ?.removeEventListener('click', this._increment);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'value') this._count = parseInt(newValue) || 0;
    if (name === 'step') this._step = parseInt(newValue) || 1;
    this._render();
  }

  _increment() {
    this._count += this._step;
    this.setAttribute('value', this._count);
    this.dispatchEvent(new CustomEvent('change', {
      detail: { value: this._count },
      bubbles: true,
      composed: true
    }));
  }

  _render() {
    this._shadow.innerHTML = `
      <style>
        button { padding: 0.5rem 1rem; cursor: pointer; }
        span { font-variant-numeric: tabular-nums; }
      </style>
      <button>+</button>
      <span>${this._count}</span>
    `;
  }
}

customElements.define('counter-element', CounterElement);

Die häufigsten Lifecycle-Fehler, die Claude Code von vornherein vermeidet:

Customized Built-in Elements

Neben autonomen Custom Elements gibt es customized built-ins — Erweiterungen bestehender HTML-Elemente. Claude Code kennt beide Formen und wählt die richtige je nach Anforderung:

// Autonomes Custom Element (universell unterstuetzt)
class MyButton extends HTMLElement { ... }
customElements.define('my-button', MyButton);

// Customized Built-in (Safari unterstuetzt das NICHT)
class FancyButton extends HTMLButtonElement { ... }
customElements.define('fancy-button', FancyButton, { extends: 'button' });
// Einsatz: <button is="fancy-button"></button>

Safari und customized built-ins: Apple unterstützt customized built-in elements (das is=""-Attribut) nicht und hat erklärt, das auch nicht zu implementieren. Für produktiven Einsatz: autonome Custom Elements bevorzugen. Claude Code weist bei customized built-ins automatisch auf diese Einschränkung hin.

3. Shadow DOM und CSS Encapsulation: open/closed, CSS Custom Properties, ::part und ::slotted

Shadow DOM ist die mächtigste und am häufigsten missverstandene Browser-API im Web Components Stack. Er erzeugt einen abgekapselten DOM-Baum: CSS von außen hat keinen Zugriff auf Selektoren im Shadow DOM, und CSS im Shadow DOM wirkt nicht nach außen. Das ist echte Kapselung — nicht die konventionelle CSS-Modul-Kapselung, die nur Klassennamen umbenennt.

open vs. closed mode

Shadow DOM gibt es in zwei Modi. Claude Code empfiehlt fast immer open:

// open: shadowRoot ueber element.shadowRoot von aussen erreichbar
this.attachShadow({ mode: 'open' });

// closed: shadowRoot ist null von aussen, kein externer JS-Zugriff
this.attachShadow({ mode: 'closed' });

Der Grund für die Empfehlung zu open: closed verhindert JS-Zugriff von außen, gibt aber keine echte Sicherheitsgarantie (DevTools sehen alles). Gleichzeitig bricht closed viele legitime Werkzeuge: Accessibility-Testing, automatisierte Tests mit querySelector, Storybook-Addons. Open ist transparenter und testbarer.

CSS Custom Properties als öffentliche Styling-API

Shadow DOM kapselt CSS — aber CSS Custom Properties (Variables) durchdringen Shadow DOM-Grenzen. Das ist der offizielle Mechanismus für Theming:

/* Im Shadow DOM der Komponente */
:host {
  display: block;
  background: var(--card-bg, #ffffff);
  color: var(--card-text, #000000);
  border-radius: var(--card-radius, 8px);
  padding: var(--card-padding, 1rem);
}

/* Von aussen steuerbar ohne Shadow DOM zu durchbrechen */
my-card {
  --card-bg: #1a1a2e;
  --card-text: #e0e0e0;
  --card-radius: 12px;
}

Claude Code generiert CSS Custom Properties automatisch als Styling-API für jede Komponente — mit sinnvollen Defaults und einer klaren externen API. Es kommentiert die verfügbaren Properties direkt in der Komponentendatei.

Das ::part Pseudo-Element

Für komplexere Styling-Szenarien gibt es ::part(): Elemente im Shadow DOM, die mit dem part-Attribut markiert werden und von außen via ::part() vollständig stylebare sind:

/* Im Shadow DOM */
<button part="trigger">Klick mich</button>
<div part="content">Inhalt</div>

/* Von aussen: voller CSS-Zugriff auf markierte Elemente */
my-dropdown::part(trigger) {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 6px;
}

my-dropdown::part(content) {
  border: 2px solid #667eea;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}

::part() gibt externem CSS Zugriff auf spezifische Elemente ohne den gesamten Shadow DOM zu öffnen. Claude Code setzt part-Attribute systematisch für alle Elemente, die Konsumenten vermutlich stylen wollen.

Das ::slotted Pseudo-Element

Um geslotteten Inhalt aus dem Shadow DOM heraus zu stylen, gibt es ::slotted():

/* Im Shadow DOM der Komponente */
::slotted(*) {
  /* Alle geslotteten Elemente */
  box-sizing: border-box;
}

::slotted(p) {
  /* Nur geslottete Paragraphen */
  margin: 0;
  line-height: 1.6;
}

::slotted(.highlight) {
  /* Geslottete Elemente mit .highlight-Klasse */
  background: rgba(212, 168, 67, 0.15);
  padding: 0.2em 0.4em;
  border-radius: 3px;
}

Cascade und Spezifizität: ::slotted()-Regeln im Shadow DOM haben niedrigere Spezifizität als externe Styles auf demselben Element. Externe CSS schlägt Shadow DOM ::slotted() immer. Das ist gewollt: Konsumenten behalten die Kontrolle über ihren Inhalt.

4. HTML Templates und Slots: template-Element, slot, named Slots und das slotchange-Event

Das <template>-Element ist ein Browser-nativer Mechanismus für inertes HTML: der Inhalt wird geparst, aber nicht gerendert und nicht ins DOM eingefügt, bis die Komponente explizit eine Instanz klont. Das macht Templates schnell — teurer DOM-Parsing-Overhead entfällt bei jeder weiteren Instanz.

Template mit Shadow DOM kombinieren

const template = document.createElement('template');
template.innerHTML = `
  <style>
    :host { display: block; font-family: inherit; }
    .card { border: 1px solid var(--border, #e0e0e0); border-radius: 8px; overflow: hidden; }
    .card-header { padding: 1rem; background: var(--header-bg, #f5f5f5); font-weight: 600; }
    .card-body { padding: 1.25rem; }
  </style>
  <div class="card">
    <div class="card-header">
      <slot name="header">Kein Titel</slot>
    </div>
    <div class="card-body">
      <slot></slot>
    </div>
  </div>
`;

class CardElement extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(template.content.cloneNode(true));
  }
}

customElements.define('card-element', CardElement);

Wichtig: template.content.cloneNode(true) erstellt eine vollständige Kopie des Template-Inhalts. Die ursprüngliche Template-Instanz bleibt unberührt — das ist der Performance-Vorteil: DOM-Struktur einmal erstellen, beliebig oft klonen.

Named Slots für strukturierten Inhalt

Named Slots erlauben Konsumenten, Inhalt gezielt in bestimmte Bereiche der Komponente einzufügen:

<!-- Komponenten-Template: media-card -->
<figure>
  <slot name="image"></slot>
  <figcaption>
    <slot name="title"></slot>
    <slot name="description"></slot>
  </figcaption>
</figure>

<!-- Konsument -->
<media-card>
  <img slot="image" src="photo.jpg" alt="Foto">
  <h2 slot="title">Artikeltitel</h2>
  <p slot="description">Kurzbeschreibung des Artikels.</p>
</media-card>

Elemente ohne slot=""-Attribut landen im Default-Slot (dem <slot> ohne name-Attribut). Elemente mit slot="name" landen im passenden Named Slot. Nicht zugeordnete Named Slots zeigen ihren Fallback-Inhalt — den Claude Code automatisch sinnvoll befüllt.

Das slotchange-Event

Wenn sich der geslottete Inhalt ändert, feuert das slotchange-Event auf dem Slot-Element — nützlich, wenn die Komponente auf Änderungen am Inhalt reagieren muss:

connectedCallback() {
  const slot = this.shadowRoot.querySelector('slot');

  slot.addEventListener('slotchange', (e) => {
    const nodes = slot.assignedNodes({ flatten: true });
    const textContent = nodes
      .filter(n => n.nodeType === Node.TEXT_NODE)
      .map(n => n.textContent.trim())
      .join('');

    // Reagiere auf geaenderten Inhalt, z. B. fuer Accessibility
    this.setAttribute('aria-label', textContent);
  });
}

slot.assignedNodes() gibt die aktuell zugewiesenen Knoten zurück. Mit { flatten: true } werden verschachtelte Slots aufgelöst. Claude Code verwendet dieses Muster automatisch, wenn Komponenten auf Inhaltsänderungen reagieren müssen — zum Beispiel für Accessibility-Attribute oder dynamische Layouts basierend auf dem Inhalt.

5. Web Components mit Claude Code: KI-gestützte Entwicklung, Testing und Storybook

Der größte praktische Vorteil von Claude Code bei Web Components ist nicht allein die API-Kenntnis — es ist die Fähigkeit, eine vollständige Komponente zu bauen, die sofort produktionsreif ist: mit Accessibility, mit Tests, mit Dokumentation, mit einer durchdachten Public API.

Vollständige Komponenten-Prompts

claude "Erstelle eine vollstaendige 'data-table' Web Component:
- Shadow DOM mit CSS Custom Properties fuer Theming
- Attribute: sortable, paginated, rows-per-page
- Slot fuer custom column header
- Custom Events: sort-change, page-change, row-click
- ARIA-Attribute fuer Screen Reader
- Keyboard-Navigation (Pfeiltasten, Enter, Space)
- Keine externen Abhaengigkeiten"

Claude Code generiert eine Komponente, die alle genannten Anforderungen erfüllt — inklusive der Dinge, die man vergisst zu nennen: role="grid" und aria-rowcount für Screen Reader, tabindex-Management für Tastatur-Navigation, composed: true auf Custom Events, saubere Attribute-zu-Property-Synchronisierung.

Web Components testen

Web Components brauchen eine echte Browser-Umgebung für Tests — jsdom unterstützt Shadow DOM nicht vollständig. Claude Code kennt die richtigen Tools:

claude "Schreibe Tests fuer die counter-element Web Component mit
Web Test Runner (@web/test-runner) und @open-wc/testing. Teste:
- Initialzustand
- Klick erhoeht den Zaehler
- value-Attribut aendert den Zaehler
- change-Event wird mit korrektem detail gefeuert"

Claude Code generiert Tests mit @open-wc/testing — dem Standard-Testing-Framework für Web Components — und setzt fixture() korrekt ein, das Shadow DOM vollständig unterstützt:

import { fixture, html, expect } from '@open-wc/testing';
import './counter-element.js';

describe('counter-element', () => {
  it('startet bei 0', async () => {
    const el = await fixture(html`<counter-element></counter-element>`);
    const span = el.shadowRoot.querySelector('span');
    expect(span.textContent).to.equal('0');
  });

  it('erhoeht bei Klick', async () => {
    const el = await fixture(html`<counter-element></counter-element>`);
    el.shadowRoot.querySelector('button').click();
    await el.updateComplete;
    expect(el.shadowRoot.querySelector('span').textContent).to.equal('1');
  });

  it('feuert change-Event', async () => {
    const el = await fixture(html`<counter-element></counter-element>`);
    let detail;
    el.addEventListener('change', e => { detail = e.detail; });
    el.shadowRoot.querySelector('button').click();
    expect(detail.value).to.equal(1);
  });
});

Storybook für Web Components

Storybook unterstützt Web Components nativ mit dem @storybook/web-components-Package. Claude Code generiert Stories direkt im richtigen Format:

claude "Schreibe Storybook Stories fuer die tooltip-element Web Component.
Zeige: Default, mit custom Styles via CSS Custom Properties,
mit langem Inhalt, auf dunklem Hintergrund."
// tooltip-element.stories.js
import { html } from 'lit';
import './tooltip-element.js';

export default {
  title: 'Components/TooltipElement',
  component: 'tooltip-element',
  argTypes: {
    position: { control: 'select', options: ['top', 'bottom', 'left', 'right'] }
  }
};

export const Default = {
  render: (args) => html`
    <tooltip-element .position=${args.position}>
      <span>Hover mich</span>
      <span slot="tooltip">Ich bin ein Tooltip</span>
    </tooltip-element>
  `
};

export const CustomStyles = {
  render: () => html`
    <style>
      .themed { --tooltip-bg: #5c6bc0; --tooltip-color: white; }
    </style>
    <tooltip-element class="themed">
      <button>Mit custom Styles</button>
      <span slot="tooltip">Eigenes Theme</span>
    </tooltip-element>
  `
};

Accessibility zuerst: Claude Code fügt bei jeder Web Component automatisch relevante ARIA-Attribute hinzu. Für interaktive Elemente: role, aria-expanded, aria-controls. Für Formularelemente: aria-label oder aria-labelledby. Das ist kein Nachgedanke — und mit Claude Code auch keiner.

6. Interoperabilität und Frameworks: React, Vue, Angular und Lit

Web Components sind per Definition framework-agnostisch. In der Praxis gibt es aber Interoperabilitätsfallen, die Claude Code kennt und vermeidet — besonders bei React, das historisch Probleme mit Custom Events und komplexen Datentypen als Attribute hatte.

Web Components in React verwenden

React 19 löst die meisten historischen Probleme. Für React 18 braucht man Wrapper-Komponenten, die Claude Code auf Anfrage generiert:

claude "Erstelle einen React-Wrapper fuer die 'data-table' Web Component.
React 18. Custom Events zu React-Props mappen, Arrays und Objekte
korrekt als Properties (nicht Attribute) setzen."
// DataTableWrapper.jsx (React 18)
import { useRef, useEffect } from 'react';
import 'data-table-element';

export function DataTable({ data, onSort, onRowClick, ...props }) {
  const ref = useRef(null);

  // Arrays/Objekte als Properties setzen (nicht als Attribute)
  useEffect(() => {
    if (ref.current) ref.current.data = data;
  }, [data]);

  // Custom Events zu React-Callbacks mappen
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const handleSort = (e) => onSort?.(e.detail);
    const handleClick = (e) => onRowClick?.(e.detail);
    el.addEventListener('sort-change', handleSort);
    el.addEventListener('row-click', handleClick);
    return () => {
      el.removeEventListener('sort-change', handleSort);
      el.removeEventListener('row-click', handleClick);
    };
  }, [onSort, onRowClick]);

  return <data-table ref={ref} {...props} />;
}

Vue und Angular: einfachere Integration

Vue und Angular haben bessere native Unterstützung für Web Components. Vue versteht Custom Events direkt, Angular seit Version 14 ebenfalls:

<!-- Vue 3: direkt verwendbar, kein Wrapper noetig -->
<template>
  <data-table
    :rows-per-page="25"
    @sort-change="handleSort"
    @row-click="handleRowClick"
  />
</template>
// Angular: CUSTOM_ELEMENTS_SCHEMA aktivieren
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}

Lit: der produktivere Entwicklungsweg

Für komplexere Web Components empfiehlt Claude Code Lit (früher LitElement). Lit ist eine minimalistische Bibliothek von Google, die das Web Components API wraps und Reaktivität, Templating und Lifecycle-Management deutlich vereinfacht — ohne die Framework-Abhängigkeit eines vollständigen Frameworks:

claude "Konvertiere die counter-element Web Component zu Lit.
Nutze @property fuer reaktive Properties, @query fuer DOM-Zugriff,
css-Tagged-Template fuer Styles."
import { LitElement, html, css } from 'lit';
import { property } from 'lit/decorators.js';

class CounterElement extends LitElement {
  static styles = css`
    :host { display: inline-flex; align-items: center; gap: 0.5rem; }
    button { padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px; }
    span { font-variant-numeric: tabular-nums; min-width: 3ch; text-align: center; }
  `;

  @property({ type: Number }) value = 0;
  @property({ type: Number }) step = 1;

  render() {
    return html`
      <button @click=${this._decrement}>-</button>
      <span>${this.value}</span>
      <button @click=${this._increment}>+</button>
    `;
  }

  _increment() {
    this.value += this.step;
    this._emitChange();
  }

  _decrement() {
    this.value -= this.step;
    this._emitChange();
  }

  _emitChange() {
    this.dispatchEvent(new CustomEvent('change', {
      detail: { value: this.value },
      bubbles: true,
      composed: true
    }));
  }
}

customElements.define('counter-element', CounterElement);

Lit-basierte Komponenten sind immer noch echte Custom Elements, die in jedem Framework und in plain HTML funktionieren. Die Bundle-Größe liegt bei etwa 6 KB minified und gzipped — ein akzeptabler Trade-off für die Produktivitätsgewinne bei komplexeren Komponenten.

Design System mit Web Components und Claude Code aufbauen

Für ein vollständiges Design System empfiehlt sich eine strukturierte Vorgehensweise mit Claude Code:

claude "Erstelle ein Design-System-Foundation-Setup mit Web Components:
- Base-Klasse mit shared Styles und CSS Custom Properties Token-System
- Component-Registry fuer automatische Element-Registrierung
- Lit als Basis, TypeScript-Dekoratoren
- TypeScript-Typen fuer alle Public Properties und Custom Events
- Storybook-Konfiguration fuer alle Komponenten"
"Web Components sind die einzige Komponentenform, die wirklich framework-agnostisch ist. Ein Design System aus Web Components funktioniert in React, Vue, Angular, plain HTML und serverseitig gerendertem Markup. Claude Code hilft, dieses System schnell und korrekt aufzubauen — mit den richtigen Browser-APIs, der richtigen Kapselung und einer durchdachten Public API."

Der vollständige Workflow — von der ersten Komponentenbeschreibung bis zum fertigen, getesteten, dokumentierten Element — dauert mit Claude Code einen Bruchteil der Zeit im Vergleich zu manueller Entwicklung. Nicht weil Claude Code schneller tippt, sondern weil es die Browser-API vollständig kennt, typische Fehler vermeidet und gleichzeitig Accessibility, Testing und Dokumentation mitliefert, ohne dass man explizit danach fragen muss.


Web Components sind die robusteste Komponentenform für langlebige, framework-agnostische UI-Bibliotheken. Mit Claude Code sind sie auch die produktivste — weil das gesamte Wissen über Custom Elements API, Shadow DOM, Slots und Interoperabilitätsfallen direkt verfügbar ist, ohne Dokumentation zu durchsuchen oder typische Fehler selbst zu machen.

Kurs · Claude Code Mastery

Web Components, Agents, MCP und mehr — auf Deutsch

Komponentenentwicklung. Testing. CI/CD. Multi-Agent-Workflows. Alles auf Deutsch, einmalig bezahlt — kein Abo, keine Plattformabhängigkeit.

Jetzt einsteigen → Kursübersicht ansehen →

Basis €29 · Pro €49 · Einmalzahlung · Kein Abo · 14 Tage Rückgaberecht