Wie eine moderne Component Library Web Components ersetzt – ohne Framework-Lock-in
SerieWeb Components
Teil 3 von 4
Im vorherigen Teil haben wir Web Components von Grund auf gebaut – Shadow DOM, Custom Events, State Management. Das funktioniert, erfordert aber viel Boilerplate.
Dieser Artikel zeigt einen anderen Ansatz: Headless Components. Statt eigene Custom Elements zu definieren, nutzen wir eine Library, die Verhalten an existierendes HTML bindet. Als Beispiel dient CASOON Atlas, eine SSR-sichere UI-Library für Tailwind v4.
Headless vs. Web Components
Der Unterschied ist fundamental:
| Aspekt | Web Components | Headless Components |
|---|---|---|
| HTML | Eigene Custom Elements (<my-modal>) | Standard-HTML (<div>, <dialog>) |
| Styling | Shadow DOM kapselt Styles | Entwickler kontrolliert CSS vollständig |
| Verhalten | In der Komponente gekapselt | JavaScript-Funktionen binden an HTML |
| SSR | Problematisch (kein DOM) | SSR-safe by design |
Headless Components sind keine Konkurrenz zu Web Components – sie lösen ein anderes Problem. Web Components eignen sich für portable, wiederverwendbare Widgets. Headless Components für Projekte, die volle Kontrolle über Markup und Styling brauchen.
Atlas installieren
npm install @casoon/atlas-components
Oder das komplette Paket mit Styles und Effects:
npm install @casoon/atlas
Modal: Der klassische Anwendungsfall
Ein Modal ist komplex: Fokus-Trapping, Escape-Taste, Backdrop-Klick, Scroll-Lock, ARIA-Attribute. Das selbst zu bauen ist aufwendig und fehleranfällig.
Das HTML
Atlas erwartet standard HTML – keine speziellen Attribute:
<button id="open-modal">Modal öffnen</button>
<div id="my-modal" style="display: none;">
<div class="modal-content">
<h2 id="modal-title">Bestätigung</h2>
<p id="modal-desc">Möchten Sie diese Aktion wirklich durchführen?</p>
<button id="close-modal">Abbrechen</button>
<button id="confirm-modal">Bestätigen</button>
</div>
</div>
Das JavaScript
import { createModal } from '@casoon/atlas-components';
const modalElement = document.getElementById('my-modal');
const openButton = document.getElementById('open-modal');
const closeButton = document.getElementById('close-modal');
const modal = createModal(modalElement, {
backdrop: true,
closeOnBackdrop: true,
closeOnEscape: true,
trapFocus: true,
backdropBlur: true,
ariaLabelledBy: 'modal-title',
ariaDescribedBy: 'modal-desc',
onOpen: () => console.log('Modal geöffnet'),
onClose: () => console.log('Modal geschlossen'),
});
openButton.addEventListener('click', () => modal.open());
closeButton.addEventListener('click', () => modal.close());
Was Atlas automatisch macht
Mit diesen wenigen Zeilen bekommt man:
- Fokus-Trapping: Tab-Navigation bleibt im Modal
- Escape-Taste: Schließt das Modal
- Backdrop-Klick: Schließt das Modal
- Scroll-Lock: Body scrollt nicht mehr
- ARIA-Attribute:
aria-modal,aria-hidden,aria-labelledby,aria-describedby - Screen-Reader-Announcements: “Dialog opened”, “Dialog closed”
- Animationen: Fade + Scale mit konfigurierbarem Timing
Das ist der Vorteil von Headless: Die Library kümmert sich um das Verhalten, das Styling bleibt beim Entwickler.
Integration in Astro
Als Astro-Komponente
---
// src/components/Modal.astro
interface Props {
id: string;
title: string;
description?: string;
}
const { id, title, description } = Astro.props;
---
<div id={id} class="modal" style="display: none;">
<div class="modal-content bg-white rounded-xl shadow-2xl p-6 max-w-md">
<h2 id={`${id}-title`} class="text-xl font-semibold mb-4">{title}</h2>
{description && <p id={`${id}-desc`} class="text-gray-600 mb-6">{description}</p>}
<slot />
<button data-modal-close class="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">
Schließen
</button>
</div>
</div>
<script>
import { createModal } from '@casoon/atlas-components';
document.querySelectorAll('.modal').forEach((el) => {
const id = el.id;
const modal = createModal(el as HTMLElement, {
trapFocus: true,
closeOnEscape: true,
ariaLabelledBy: `${id}-title`,
ariaDescribedBy: `${id}-desc`,
});
// Trigger-Buttons finden
document.querySelectorAll(`[data-modal-trigger="${id}"]`).forEach((trigger) => {
trigger.addEventListener('click', () => modal.open());
});
// Close-Button im Modal
el.querySelector('[data-modal-close]')?.addEventListener('click', () => modal.close());
});
</script>
<style>
.modal {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
</style>
Nutzung
---
import Modal from '../components/Modal.astro';
---
<button data-modal-trigger="confirm-modal">Löschen</button>
<Modal id="confirm-modal" title="Wirklich löschen?" description="Diese Aktion kann nicht rückgängig gemacht werden.">
<div class="flex gap-4">
<button data-modal-close class="px-4 py-2 bg-gray-200 rounded">Abbrechen</button>
<button class="px-4 py-2 bg-red-600 text-white rounded">Löschen</button>
</div>
</Modal>
Tabs: Komplexere Interaktionen
Tabs benötigen Keyboard-Navigation (Pfeiltasten), ARIA-Rollen und synchronisierten State.
HTML-Struktur
<div id="product-tabs">
<div role="tablist" class="flex border-b">
<button role="tab" aria-selected="true" aria-controls="panel-details">Details</button>
<button role="tab" aria-selected="false" aria-controls="panel-specs">Spezifikationen</button>
<button role="tab" aria-selected="false" aria-controls="panel-reviews">Bewertungen</button>
</div>
<div id="panel-details" role="tabpanel">
<p>Produktbeschreibung hier...</p>
</div>
<div id="panel-specs" role="tabpanel" hidden>
<table>...</table>
</div>
<div id="panel-reviews" role="tabpanel" hidden>
<div class="reviews">...</div>
</div>
</div>
JavaScript
import { createTabs } from '@casoon/atlas-components';
const tabs = createTabs(document.getElementById('product-tabs'), {
orientation: 'horizontal',
activateOnFocus: true,
onChange: (index) => {
console.log(`Tab ${index} aktiviert`);
// Analytics, URL-Update etc.
},
});
// Programmatisch wechseln
tabs.setActiveTab(1);
Was Atlas übernimmt
- Keyboard-Navigation: Pfeiltasten wechseln Tabs
- ARIA-Updates:
aria-selected, Panel-Visibility - Focus-Management: Fokus folgt der Auswahl
- Orientation: Horizontal oder vertikal
Toast-Notifications
Toasts sind schwierig: Stacking, Auto-Dismiss, Screen-Reader-Announcements. Atlas bietet einen Toast-Manager.
import { createToastManager } from '@casoon/atlas-components';
const toasts = createToastManager({
position: 'top-right',
maxToasts: 5,
defaultDuration: 5000,
});
// Toast anzeigen
toasts.show({
title: 'Gespeichert',
message: 'Ihre Änderungen wurden übernommen.',
type: 'success',
});
// Fehler-Toast
toasts.show({
title: 'Fehler',
message: 'Verbindung zum Server fehlgeschlagen.',
type: 'error',
duration: 0, // Bleibt bis manuell geschlossen
});
Der Manager kümmert sich um:
- Stacking: Mehrere Toasts übereinander
- Animation: Ein-/Ausblenden
- Auto-Dismiss: Nach konfigurierter Zeit
- Screen-Reader:
aria-live="polite"Announcements
Accordion
import { createAccordion } from '@casoon/atlas-components';
const accordion = createAccordion(document.getElementById('faq'), {
allowMultiple: false, // Nur ein Panel offen
animation: 'normal',
});
// Programmatisch öffnen/schließen
accordion.open(0);
accordion.closeAll();
Vergleich: Eigene Web Component vs. Atlas
Selbst gebaut (aus Teil 1)
class MyModal extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// 200+ Zeilen für Fokus-Trapping, ARIA, Animation...
}
}
customElements.define('my-modal', MyModal);
Mit Atlas
import { createModal } from '@casoon/atlas-components';
const modal = createModal(element, options);
Der Unterschied:
- Web Component: Volle Kontrolle, aber mehr Arbeit
- Atlas: Bewährtes Verhalten, weniger Code
SSR-Sicherheit
Atlas ist für Server-Side Rendering designed. Alle Funktionen prüfen auf Browser-Umgebung:
// Intern in jeder Komponente
if (!isBrowser()) {
return createNoopState(element);
}
Bei SSR wird ein No-Op-Objekt zurückgegeben – keine Fehler, keine DOM-Zugriffe. Die Initialisierung erfolgt erst im Browser.
In Astro funktioniert das nahtlos:
<script>
// Wird nur im Browser ausgeführt
import { createModal } from '@casoon/atlas-components';
const modal = createModal(document.getElementById('my-modal'));
</script>
Wann Web Components, wann Headless?
Web Components wählen wenn:
- Die Komponente in verschiedenen Projekten/Frameworks genutzt wird
- Style-Kapselung wichtig ist
- Ein eigenständiges Widget benötigt wird (Embed, Widget)
Headless Components wählen wenn:
- Volle Kontrolle über Markup und Styling gewünscht ist
- Das Projekt ein Design-System mit eigenem CSS hat
- SSR kritisch ist
- Accessibility wichtig ist und man sich auf eine Library verlassen will
Praktisches Beispiel: Product Page
Eine typische E-Commerce-Produktseite kombiniert mehrere Komponenten:
---
import Layout from '../layouts/Layout.astro';
import { getProduct } from '../lib/products';
const product = await getProduct(Astro.params.id);
---
<Layout title={product.name}>
<div class="product-page">
<!-- Image Gallery mit Carousel -->
<div id="gallery">
{product.images.map((img, i) => (
<img src={img} alt={`${product.name} Bild ${i + 1}`} />
))}
</div>
<!-- Produkt-Info -->
<div class="product-info">
<h1>{product.name}</h1>
<p class="price">{product.price} €</p>
<!-- Varianten-Auswahl mit Tabs -->
<div id="variant-tabs">
<div role="tablist">
{product.variants.map((v) => (
<button role="tab">{v.name}</button>
))}
</div>
{product.variants.map((v) => (
<div role="tabpanel">
<p>{v.description}</p>
<span>{v.price} €</span>
</div>
))}
</div>
<button id="add-to-cart">In den Warenkorb</button>
</div>
<!-- Details Accordion -->
<div id="product-accordion">
<div data-accordion-item>
<button data-accordion-trigger>Beschreibung</button>
<div data-accordion-content>{product.description}</div>
</div>
<div data-accordion-item>
<button data-accordion-trigger>Spezifikationen</button>
<div data-accordion-content>
<table>...</table>
</div>
</div>
</div>
</div>
<!-- Bestätigungs-Modal -->
<div id="cart-modal" style="display: none;">
<h2>Zum Warenkorb hinzugefügt</h2>
<p>{product.name} wurde hinzugefügt.</p>
<button data-modal-close>Weiter einkaufen</button>
<a href="/cart">Zum Warenkorb</a>
</div>
</Layout>
<script>
import { createTabs, createAccordion, createModal, createCarousel } from '@casoon/atlas-components';
// Gallery Carousel
const gallery = createCarousel(document.getElementById('gallery'), {
autoplay: false,
loop: true,
});
// Varianten Tabs
const tabs = createTabs(document.getElementById('variant-tabs'));
// Accordion
const accordion = createAccordion(document.getElementById('product-accordion'), {
allowMultiple: true,
});
// Cart Modal
const modal = createModal(document.getElementById('cart-modal'));
document.getElementById('add-to-cart')?.addEventListener('click', () => {
// Cart-Logik...
modal.open();
});
</script>
Repository
Der vollständige Code zu diesem Artikel findet sich im Repository:
GitHub: github.com/casoon/astro-webcomponents-tutorial
astro-webcomponents-tutorial/
├── part1/ # Vanilla Web Components (Teil 1)
└── part2/ # Atlas Components (dieser Artikel)
Ressourcen
- CASOON Atlas – Die verwendete Component Library
- Atlas Components Docs – API-Dokumentation
- WAI-ARIA Authoring Practices – Accessibility-Patterns