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:

  1. Callback-Prop durchreichen: Eltern -> … -> Kind -> Button. Wenn dazwischen viele Schichten liegen, ist das ein Schmerz (Stichwort: „Prop-Drilling”).
  2. 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:

svelte AddButton.svelte
<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:

  1. new CustomEvent('item-added', ...) erzeugt ein Event-Objekt mit dem Namen item-added.
  2. detail: { ... } ist ein Objekt mit beliebigen Daten. Das ist die „Nutzlast” des Events.
  3. bubbles: true erlaubt dem Event, im DOM-Baum nach oben zu wandern. Ohne das bleibt es am Button und kann nicht von Eltern eingefangen werden.
  4. 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:

svelte Eltern-Komponente
<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, nicht onItemAdded. HTML-Attribute sind case-insensitive — Svelte folgt dieser Konvention.
  • event.detail liefert 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

ts src/lib/toast.ts
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

svelte src/lib/components/ToastContainer.svelte
<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

svelte Irgendeine andere Komponente
<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:

ts Typisierter CustomEvent
type ToastDetail = {
    message: string;
    type: 'info' | 'error';
};

export function showToast(detail: ToastDetail) {
    window.dispatchEvent(
        new CustomEvent<ToastDetail>('toast', { detail })
    );
}
svelte Im Listener
<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:

svelte Capture-Phase
<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:

ts Lokales Custom Event
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:

ts Shadow-DOM-übergreifend
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.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu Events

Zur Übersicht