Manchmal brauchst du etwas, das wie ein DOM-Event aussieht, aber nicht vom Browser ausgelöst wird — sondern von dir, mit eigenem Namen und eigenen Daten. Das ist das Einsatzgebiet der CustomEvent-API. Sie ist Standard-Browser-Funktionalität (kein Svelte-Konzept), passt aber wunderbar mit Sveltes Event-Syntax zusammen. Dieser Artikel erklärt Schritt für Schritt, wie das funktioniert, wozu man es braucht und wann es nicht das richtige Werkzeug ist.
Was ist ein Custom Event?
Stell dir vor, im DOM-Baum hängt ein Button, irgendwo darüber ein <div>, und ganz oben eine Komponente. Wenn der Button etwas Spezielles auslösen soll — z. B. „der Nutzer hat ein Element zur Liste hinzugefügt” — dann gibt es zwei Wege:
- Callback-Prop durchreichen: Eltern -> … -> Kind -> Button. Wenn dazwischen viele Schichten liegen, ist das ein Schmerz (Stichwort: „Prop-Drilling”).
- Custom Event abschicken, der durchs DOM nach oben bubbelt und an jeder beliebigen Stelle eingefangen werden kann — ohne dass die Zwischenkomponenten davon wissen.
Variante 2 ist genau das, was Custom Events bieten.
Ein Custom Event erzeugen und auslösen
Das einfachste Beispiel:
<script>
let buttonElement;
function fire() {
const customEvent = new CustomEvent('item-added', {
detail: { id: 42, name: 'Brot' },
bubbles: true,
});
buttonElement.dispatchEvent(customEvent);
}
</script>
<button bind:this={buttonElement} onclick={fire}>
Eintrag hinzufügen
</button>Was hier Schritt für Schritt passiert:
new CustomEvent('item-added', ...)erzeugt ein Event-Objekt mit dem Namenitem-added.detail: { ... }ist ein Objekt mit beliebigen Daten. Das ist die „Nutzlast” des Events.bubbles: trueerlaubt dem Event, im DOM-Baum nach oben zu wandern. Ohne das bleibt es am Button und kann nicht von Eltern eingefangen werden.buttonElement.dispatchEvent(customEvent)schickt das Event ab.
Das Event einfangen
Auf jedem Eltern-Element kannst du jetzt einen Handler hängen, der wie ein normaler DOM-Event-Handler aussieht — Svelte behandelt unbekannte on…-Attribute als Event-Listener:
<script>
import AddButton from '$lib/AddButton.svelte';
function handleAdded(event) {
// event.detail enthält das Objekt aus CustomEvent
console.log('Hinzugefügt:', event.detail.name);
}
</script>
<div onitem-added={handleAdded}>
<AddButton />
</div>Wichtig:
- Der Attribut-Name ist klein geschrieben:
onitem-added, nichtonItemAdded. HTML-Attribute sind case-insensitive — Svelte folgt dieser Konvention. event.detailliefert das Objekt, das du beim Erzeugen mitgegeben hast.- Der Listener darf an jedem beliebigen Vorfahren hängen — er muss nicht der direkte Eltern sein.
Praktisches Beispiel: Toast-Nachrichten
Ein gutes Anwendungsbeispiel: Aus jeder beliebigen Komponente eine Toast-Nachricht auslösen, ohne dass die Komponente weiß, wo der Toast angezeigt wird.
Schritt 1 – Helper-Funktion zum Auslösen
export function showToast(message: string, type: 'info' | 'error' = 'info') {
window.dispatchEvent(
new CustomEvent('toast', {
detail: { message, type },
})
);
}window.dispatchEvent schickt das Event auf Window-Ebene los — von dort kann es überall eingefangen werden.
Schritt 2 – Toast-Container-Komponente
<script>
let toasts = $state([]);
let nextId = 1;
function add(event) {
const id = nextId++;
toasts.push({ id, ...event.detail });
setTimeout(() => {
toasts = toasts.filter((t) => t.id !== id);
}, 3000);
}
</script>
<svelte:window ontoast={add} />
<div class="toast-container">
{#each toasts as toast (toast.id)}
<div class={`toast toast-${toast.type}`}>
{toast.message}
</div>
{/each}
</div>Schritt 3 – Aus beliebiger Komponente auslösen
<script>
import { showToast } from '$lib/toast';
async function save() {
try {
await fetch('/api/save');
showToast('Gespeichert!', 'info');
} catch {
showToast('Fehler beim Speichern', 'error');
}
}
</script>
<button onclick={save}>Speichern</button>Der Aufrufer von showToast weiß nicht, wo der Toast erscheint. Der Container weiß nicht, wer den Toast ausgelöst hat. Beide sind voneinander entkoppelt.
TypeScript für Custom Events
Mit TypeScript kannst du den Detail-Typ festschreiben:
type ToastDetail = {
message: string;
type: 'info' | 'error';
};
export function showToast(detail: ToastDetail) {
window.dispatchEvent(
new CustomEvent<ToastDetail>('toast', { detail })
);
}<script lang="ts">
function add(event: CustomEvent<ToastDetail>) {
console.log(event.detail.type, event.detail.message);
}
</script>
<svelte:window ontoast={add} />Wann sollte man Custom Events nicht nutzen?
Custom Events sind ein nützliches Pattern, aber kein Allheilmittel. In folgenden Fällen sind andere Lösungen passender:
- Wenn nur Eltern und direktes Kind kommunizieren: Eine Callback-Prop ist klarer und besser typisierbar.
- Wenn der State zentral gehalten und an mehreren Stellen gelesen wird: Ein
$state-Container oder ein Store ist besser. - Wenn die Verbindung NICHT durchs DOM läuft (z. B. zwischen zwei unabhängigen Inseln): Custom Events bubbeln nur, wenn ein DOM-Pfad existiert. Sonst bleibt nur ein zentraler State.
- Wenn der Empfänger muss den Event verarbeiten: Custom Events sind „fire and forget”. Es gibt keine Garantie, dass jemand zuhört.
Faustregel: Erst die einfache Lösung versuchen. Custom Events sind die richtige Antwort für entkoppelte, optionale Kommunikation — nicht für Standard-Datenflüsse.
Bubbling vs. Capturing — wo geht das Event hin?
Wenn bubbles: true, wandert das Event vom Auslöser nach oben durch alle Vorfahren bis zum <html>-Element. An jedem Element kannst du es abfangen.
Wenn du es früher im Capture-Phase abfangen willst (auf dem Weg nach unten), nutzt du Sveltes capture-Variante:
<div onitem-addedcapture={handle}>
…
</div>In Praxis ist Capture für Custom Events selten — Bubble reicht fast immer.
bubbles: false für lokale Events
Wenn du explizit verhindern willst, dass ein Custom Event nach oben wandert, lass bubbles weg oder setz es auf false:
const event = new CustomEvent('local-only', { detail });
// bubbles: false ist Default
element.dispatchEvent(event);Dann hört nur ein Listener am gleichen Element davon.
composed: true — durch Shadow-DOM-Grenzen hindurch
Wenn du in einer Web Component oder im Shadow DOM arbeitest, hört das normale Bubbling an der Shadow-Grenze auf. Mit composed: true durchschreitet das Event auch diese Grenze:
const event = new CustomEvent('item-added', {
detail,
bubbles: true,
composed: true,
});Brauchst du selten — aber bei Custom Elements (<svelte:options customElement>) wichtig.
Häufige Stolperfallen
Vergessenes bubbles: true.
Wenn der Listener am Eltern hängt und nichts ankommt — das ist meistens die Ursache.
Listener mit camelCase-Namen.
onItemAdded funktioniert nicht. Es muss onitem-added (alles klein, mit Bindestrich) heißen — analog zu HTML.
Event-Name mit Leerzeichen oder Sonderzeichen. Erlaubt sind nur Buchstaben, Zahlen und Bindestriche. Wenn du beim Empfangen-Versuch keine Reaktion siehst, prüfe den Namen.
dispatchEvent auf einem nicht gemounteten Element.
Wenn das Element noch nicht im DOM ist, kann das Event nirgendwohin bubbeln. Im onMount oder $effect arbeiten.
Event als Standard-Lösung statt als Spezialfall. Wenn du anfängst, jeden Datenfluss als Custom Event zu lösen, wird die App schwer zu debuggen. Stores oder Callback-Props sind in den meisten Fällen das bessere Werkzeug.