Zum Inhalt springen

Web Components ohne Framework – Native Custom Elements in der Praxis

· von · Aktualisiert am

Web Components und Custom Elements

~16 Min. Lesezeit · Veröffentlicht am

Das Wichtigste vorab: Web Components sind Browser-native wiederverwendbare Komponenten – ohne React, ohne Vue, ohne Build-Tools. Sie funktionieren mit reinem JavaScript und HTML, sind framework-agnostisch und werden von allen modernen Browsern unterstützt. Dieser Artikel zeigt wann Web Components Sinn machen, wie sie funktionieren und wie man sie in der Praxis einsetzt.

Ich mag lightweight Lösungen. Mein Blog nutzt kein React, kein Vue, nicht mal jQuery – nur Vanilla JS und Alpine.js für minimale Interaktivität. Aber es gibt Situationen, in denen man wiederverwendbare Komponenten braucht: Ein konfigurierbarer Accordion, ein Custom Dropdown, ein Image-Slider.

Die klassische Lösung: Framework einbinden (100+ KB). Die bessere Lösung 2026: Web Components.

Was sind Web Components?

Web Components sind eine Sammlung von Web-Standards, die es ermöglichen, eigene HTML-Elemente zu erstellen – komplett mit eigenem Markup, Styling und Verhalten.

Die drei Säulen:

1. Custom Elements
Eigene HTML-Tags definieren (z.B. <user-card>)
2. Shadow DOM
Gekapseltes CSS/HTML – kein Konflikt mit globalem Styling
3. HTML Templates
Wiederverwendbare Markup-Schnipsel mit <template>

Ein einfaches Beispiel

So nutzt du eine Web Component:

<!-- HTML: Komponente nutzen -->
<user-card 
  name="Sven Muscheid" 
  role="Webentwickler"
  avatar="avatar.jpg">
</user-card>

Und so definierst du sie:

// JavaScript: Komponente definieren
class UserCard extends HTMLElement {
  connectedCallback() {
    const name = this.getAttribute('name');
    const role = this.getAttribute('role');
    const avatar = this.getAttribute('avatar');
    
    this.innerHTML = `
      <div class="user-card">
        <img src="${avatar}" alt="${name}">
        <h3>${name}</h3>
        <p>${role}</p>
      </div>
    `;
  }
}

// Komponente registrieren
customElements.define('user-card', UserCard);

Das war's. Kein npm install, kein Build-Tool, keine Dependencies. Funktioniert in jedem modernen Browser.

Warum Web Components? (Oder: Warum nicht React?)

Die ehrliche Frage: Wann brauchst du Web Components statt React/Vue/Svelte?

KriteriumWeb ComponentsReact/Vue/Svelte
Dependencies Keine (nur Browser) Framework nötig (30-100+ KB)
Build-Tools Optional Meist zwingend (Webpack, Vite)
Framework-Lock-in Framework-agnostisch An Framework gebunden
Ladezeit Minimal (nur eigener Code)~ Framework + App-Code
Browser-Support Alle modernen Browser Auch modern (mit Polyfills)
DevTools/Ecosystem~ Basic (verbessert sich) Ausgereift (DevTools, Libraries)
State Management~ Manuell (oder Library) Eingebaut (Hooks, Stores)
Reactivity~ Manuell Automatisch
Lernkurve Flach (Vanilla JS)~ Steiler (Framework-Konzepte)
Use CaseEinzelne Komponenten, Widgets, Progressive EnhancementKomplexe Apps, SPAs, viel Interaktivität
Meine Empfehlung:

Web Components wenn:

  • Du einzelne interaktive Komponenten brauchst (nicht eine ganze App)
  • Performance/Bundle-Size wichtig ist
  • Du framework-agnostisch bleiben willst
  • Progressive Enhancement wichtig ist
  • Du kein Build-Setup willst

Framework wenn:

  • Du eine komplexe SPA baust
  • Viel State Management nötig ist
  • Dein Team bereits Framework-Expertise hat
  • Du auf ein ausgereiftes Ecosystem angewiesen bist

Die drei Säulen im Detail

1. Custom Elements – Eigene HTML-Tags

Custom Elements erlauben dir, eigene HTML-Tags zu definieren. Es gibt zwei Arten:

Autonomous Custom Elements (von scratch)

// Definition
class SimpleGreeting extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<p>Hallo, Welt!</p>`;
  }
}

customElements.define('simple-greeting', SimpleGreeting);

// Nutzung
<simple-greeting></simple-greeting>

Customized Built-in Elements (erweitern bestehende Elemente)

// Definition (erweitert <button>)
class FancyButton extends HTMLButtonElement {
  connectedCallback() {
    this.addEventListener('click', () => {
      this.classList.add('clicked');
    });
  }
}

customElements.define('fancy-button', FancyButton, { extends: 'button' });

// Nutzung
<button is="fancy-button">Click me</button>
Naming-Regel: Custom Element Namen müssen einen Bindestrich enthalten (z.B. user-card, nicht usercard). Das verhindert Kollisionen mit zukünftigen HTML-Standard-Tags.

2. Shadow DOM – Style-Kapselung

Shadow DOM erstellt einen "parallelen DOM-Baum" innerhalb deiner Komponente – komplett isoliert vom Rest der Seite.

Problem ohne Shadow DOM:

// Globales CSS
.card { background: red; }

// Deine Komponente
class UserCard extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<div class="card">...</div>`;
  }
}

// Resultat: Komponente hat roten Hintergrund (ungewollt!)

Lösung mit Shadow DOM:

class UserCard extends HTMLElement {
  connectedCallback() {
    // Shadow Root erstellen
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        /* Nur für diese Komponente */
        .card {
          background: blue;
          padding: 1rem;
          border-radius: 8px;
        }
      </style>
      <div class="card">
        <h3>${this.getAttribute('name')}</h3>
      </div>
    `;
  }
}

customElements.define('user-card', UserCard);

Vorteile:

Shadow DOM Modi:
  • mode: 'open' – JavaScript kann von außen auf Shadow Root zugreifen (element.shadowRoot)
  • mode: 'closed' – Kein Zugriff von außen (selten genutzt)

3. HTML Templates – Wiederverwendbare Markup-Schnipsel

Mit <template> definierst du HTML, das initial nicht gerendert wird – perfekt für Komponenten.

<!-- HTML: Template definieren -->
<template id="user-card-template">
  <style>
    .card {
      border: 1px solid #ccc;
      padding: 1rem;
      border-radius: 8px;
    }
    .avatar {
      width: 64px;
      height: 64px;
      border-radius: 50%;
    }
  </style>
  <div class="card">
    <img class="avatar" src="" alt="">
    <h3 class="name"></h3>
    <p class="role"></p>
  </div>
</template>

<script>
class UserCard extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('user-card-template');
    const content = template.content.cloneNode(true);
    
    // Daten einfügen
    content.querySelector('.avatar').src = this.getAttribute('avatar');
    content.querySelector('.avatar').alt = this.getAttribute('name');
    content.querySelector('.name').textContent = this.getAttribute('name');
    content.querySelector('.role').textContent = this.getAttribute('role');
    
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(content);
  }
}

customElements.define('user-card', UserCard);
</script>

Praxisbeispiel: Akkordeon-Komponente

Bauen wir eine vollständige, wiederverwendbare Akkordeon-Komponente – mit Animation, Accessibility und Shadow DOM.

<template id="accordion-template">
  <style>
    :host {
      display: block;
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 8px;
      overflow: hidden;
    }
    
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem;
      background: rgba(255,255,255,0.05);
      cursor: pointer;
      user-select: none;
    }
    
    .header:hover {
      background: rgba(255,255,255,0.08);
    }
    
    .icon {
      transition: transform 0.3s;
    }
    
    .icon.open {
      transform: rotate(180deg);
    }
    
    .content {
      max-height: 0;
      overflow: hidden;
      transition: max-height 0.3s ease;
    }
    
    .content.open {
      max-height: 500px;
    }
    
    .content-inner {
      padding: 1rem;
    }
  </style>
  
  <div class="header" role="button" tabindex="0" aria-expanded="false">
    <span class="title"></span>
    <span class="icon">▼</span>
  </div>
  <div class="content">
    <div class="content-inner">
      <slot></slot>
    </div>
  </div>
</template>

<script>
class AccordionItem extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('accordion-template');
    const content = template.content.cloneNode(true);
    
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(content);
    
    this.header = shadow.querySelector('.header');
    this.contentEl = shadow.querySelector('.content');
    this.icon = shadow.querySelector('.icon');
    this.titleEl = shadow.querySelector('.title');
  }
  
  connectedCallback() {
    // Titel setzen
    this.titleEl.textContent = this.getAttribute('title') || 'Accordion Item';
    
    // Event Listener
    this.header.addEventListener('click', () => this.toggle());
    this.header.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        this.toggle();
      }
    });
  }
  
  toggle() {
    const isOpen = this.contentEl.classList.contains('open');
    
    if (isOpen) {
      this.contentEl.classList.remove('open');
      this.icon.classList.remove('open');
      this.header.setAttribute('aria-expanded', 'false');
    } else {
      this.contentEl.classList.add('open');
      this.icon.classList.add('open');
      this.header.setAttribute('aria-expanded', 'true');
    }
    
    // Custom Event dispatchen
    this.dispatchEvent(new CustomEvent('accordion-toggle', {
      detail: { open: !isOpen }
    }));
  }
}

customElements.define('accordion-item', AccordionItem);
</script>

Nutzung:

<accordion-item title="Was sind Web Components?">
  <p>Web Components sind eine Sammlung von Browser-Standards...</p>
</accordion-item>

<accordion-item title="Wie nutze ich Shadow DOM?">
  <p>Shadow DOM wird mit attachShadow() erstellt...</p>
</accordion-item>

Was passiert hier?

Lifecycle Callbacks

Web Components haben Lifecycle-Methoden ähnlich wie React/Vue:

CallbackWann wird aufgerufen?Use Case
constructor()Element wird erstelltShadow DOM erstellen, initialen State setzen. Kein DOM-Access!
connectedCallback()Element wird ins DOM eingefügtEvent Listener, DOM-Manipulation, Daten laden
disconnectedCallback()Element wird aus DOM entferntCleanup, Event Listener entfernen, Timers stoppen
attributeChangedCallback()Attribute wird geändertAuf Attribut-Änderungen reagieren (nur observierte Attribute)
adoptedCallback()Element wird in neues Document verschobenSelten genutzt (iframes, document.adoptNode)

Attribute beobachten

class UserCard extends HTMLElement {
  // Welche Attribute beobachtet werden sollen
  static get observedAttributes() {
    return ['name', 'role'];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} changed: ${oldValue} → ${newValue}`);
    
    if (name === 'name') {
      this.shadowRoot.querySelector('.name').textContent = newValue;
    }
    if (name === 'role') {
      this.shadowRoot.querySelector('.role').textContent = newValue;
    }
  }
}

Slots – Content von außen einfügen

Slots sind Platzhalter für Content, den der Nutzer der Komponente bereitstellt.

Default Slot (unnamed)

<!-- Komponente -->
<template>
  <div class="wrapper">
    <slot></slot>
  </div>
</template>

<!-- Nutzung -->
<my-component>
  <p>Dieser Content erscheint im Slot</p>
</my-component>

Named Slots

<!-- Komponente -->
<template>
  <div class="card">
    <div class="header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="body">
      <slot>Default Content</slot>
    </div>
    <div class="footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<!-- Nutzung -->
<card-component>
  <h2 slot="header">Mein Titel</h2>
  <p>Dieser Text landet im Default Slot</p>
  <button slot="footer">Action</button>
</card-component>

Web Components + Tailwind CSS

Kann man Tailwind mit Web Components nutzen? Ja, aber mit Einschränkungen.

Problem: Shadow DOM isoliert CSS

Tailwind-Klassen die außerhalb der Komponente definiert sind, funktionieren nicht innerhalb des Shadow DOM.

<!-- Funktioniert NICHT -->
<user-card class="p-4 bg-blue-500"></user-card>

// Shadow DOM blockiert globale Styles

Lösungen:

1. Kein Shadow DOM nutzen

Komponente nutzt normales DOM, Tailwind funktioniert normal. Nachteil: Kein Style-Isolation.

2. Tailwind ins Shadow DOM injecten

Tailwind-CSS-Datei per <link> oder <style> in Shadow Root einfügen.

3. CSS Custom Properties (Variablen)

Shadow DOM erbt CSS Custom Properties. Tailwind über Custom Properties stylen.

4. Constructable Stylesheets

Moderne API um Stylesheets zwischen Shadow Roots zu teilen (Browser-Support 2026: gut).

Praxis-Beispiel: Tailwind mit Constructable Stylesheets

// Einmal: Tailwind als Stylesheet erstellen
const tailwindSheet = new CSSStyleSheet();
tailwindSheet.replaceSync(`
  /* Tailwind CSS hier (oder aus File laden) */
  @import url('/path/to/tailwind.css');
`);

// In jeder Komponente nutzen
class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    
    // Tailwind-Sheet hinzufügen
    shadow.adoptedStyleSheets = [tailwindSheet];
    
    shadow.innerHTML = `
      <div class="p-4 text-white bg-blue-500 rounded">
        Tailwind funktioniert!
      </div>
    `;
  }
}

Browser-Support 2026

Exzellenter Browser-Support:
  • Chrome/Edge: Seit 2018 (v67+)
  • Firefox: Seit 2018 (v63+)
  • Safari: Seit 2020 (v14+)
  • Opera: Seit 2018 (v54+)

Support-Rate 2026: 95%+ aller Browser weltweit

Polyfill nötig? Nur für sehr alte Browser (IE11, Safari <14). In 2026 praktisch nicht mehr relevant.

Wann Web Components NICHT nutzen

Web Components sind nicht ideal für:
  • Komplexe SPAs: Kein eingebautes State Management, Routing, etc. → Framework besser
  • Server-Side Rendering: Web Components sind client-only (aber: Lit, Stencil bieten SSR)
  • Wenn das Team bereits React-Expertise hat: Don't fix what ain't broken
  • Wenn du ein ausgebautes Ecosystem brauchst: React/Vue haben mehr fertige Komponenten

Libraries für Web Components

Vanilla Web Components sind mächtig, aber manchmal willst du mehr Komfort:

LibraryWas bringt's?Bundle Size
Lit (by Google)Reactive Properties, Templating, Lifecycle-Komfort. Industry-Standard für WC.~5 KB
StencilTypeScript, JSX, Compiler (generiert optimierte WC). Ionic nutzt das.~0 KB (kompiliert weg)
HauntedReact Hooks API für Web Components (useState, useEffect, etc.)~2 KB
FAST (by Microsoft)Design System + Web Components, sehr performantVariabel
Meine Empfehlung:

Für einfache Komponenten: Vanilla Web Components (0 Dependencies)

Für mehrere komplexere Komponenten: Lit (minimaler Overhead, gute DX)

Für große Component Libraries: Stencil (best tooling, TypeScript, kompiliert zu optimierten WC)

Mein Praxis-Setup: Web Components + Alpine.js

Auf meinem Blog nutze ich eine Kombi:

Resultat: 100/100 Lighthouse Score, ~15 KB JavaScript gesamt, kein Build-Tool nötig.

<!-- Beispiel: Code-Snippet Komponente -->
<code-snippet language="javascript">
const greeting = 'Hello World';
console.log(greeting);
</code-snippet>

<script>
class CodeSnippet extends HTMLElement {
  connectedCallback() {
    const language = this.getAttribute('language') || 'text';
    const code = this.textContent.trim();
    
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        .snippet {
          position: relative;
          background: #1e1e1e;
          border-radius: 8px;
          padding: 1rem;
        }
        .copy-btn {
          position: absolute;
          top: 0.5rem;
          right: 0.5rem;
          background: rgba(255,255,255,0.1);
          border: none;
          padding: 0.5rem;
          cursor: pointer;
          border-radius: 4px;
        }
        .copy-btn:hover {
          background: rgba(255,255,255,0.2);
        }
        pre {
          margin: 0;
          overflow-x: auto;
        }
        code {
          font-family: 'Courier New', monospace;
          color: #7CD5EE;
        }
      </style>
      <div class="snippet">
        <button class="copy-btn" title="Copy to clipboard">📋</button>
        <pre><code class="language-${language}">${code}</code></pre>
      </div>
    `;
    
    // Copy to Clipboard
    shadow.querySelector('.copy-btn').addEventListener('click', async () => {
      await navigator.clipboard.writeText(code);
      // Feedback zeigen
      const btn = shadow.querySelector('.copy-btn');
      btn.textContent = '✓';
      setTimeout(() => btn.textContent = '📋', 2000);
    });
  }
}

customElements.define('code-snippet', CodeSnippet);
</script>

Fazit: Web Components in 2026

Web Components sind erwachsen geworden. Browser-Support ist exzellent, die APIs sind stabil, und für viele Use Cases sind sie die bessere Wahl als ein komplettes Framework.

Wann ich Web Components nutze:
  • Einzelne wiederverwendbare UI-Komponenten (Buttons, Cards, Accordions)
  • Widgets die in verschiedenen Projekten/Frameworks funktionieren sollen
  • Progressive Enhancement (Component funktioniert, auch wenn JS fehlschlägt)
  • Performance-kritische Seiten (kein Framework-Overhead)
  • Wenn ich kein Build-Setup will
Wann ich React/Vue nutze:
  • Komplexe SPAs mit viel State
  • Große Teams mit Framework-Expertise
  • Wenn ich auf fertiges Ecosystem angewiesen bin (Component Libraries, Router, etc.)
  • Rapid Prototyping (React DevTools, Hot Reload, etc.)

Die Zukunft ist nicht "Framework ODER Web Components" – sondern beides, für unterschiedliche Zwecke. Web Components sind das Missing Piece für lightweight, framework-agnostische UI-Komponenten.

„Web Components sind wie Unix-Tools: Mach eine Sache, mach sie gut, und spiel gut mit anderen."

Mehr zum Thema Frontend-Entwicklung: Alpine.js für dynamische Websites, Tailwind CSS Framework, TypeScript Migration.