In Svelte 4 konnte man hinter einen Event-Handler einen oder mehrere Modifier anhängen — getrennt mit |. Beispiel: on:click|preventDefault|once={save}. In Svelte 5 gibt es diese Syntax nicht mehr. Das ist erst mal ungewohnt, aber die neue Lösung ist meist sogar lesbarer: Du schreibst die Logik einfach direkt in den Handler. Dieser Artikel zeigt, wie du jeden alten Modifier sauber ersetzt — und gibt am Ende einen kleinen Helper für die Fälle, in denen das oft wiederholt wird.

Übersicht der alten Modifier

In Svelte 4 gab es diese Modifier, anhängbar mit |:

ModifierZweck
preventDefaultBrowser-Standardverhalten unterdrücken (z. B. Form-Submit-Reload).
stopPropagationEvent nicht weiter an Eltern-Elemente bubblen.
stopImmediatePropagationAuch andere Listener auf demselben Element nicht aufrufen.
onceHandler nur einmal ausführen, dann automatisch entfernen.
captureIm Capture-Phase laufen statt im Bubble-Phase.
passiveBrowser darf nicht auf preventDefault() warten (für Scroll-Performance).
nonpassiveGegenteil: explizit aktiv (überschreibt automatische Defaults).
selfNur reagieren, wenn das Event direkt am Element ausgelöst wurde.
trustedNur echte Nutzer-Events, keine programmatisch ausgelösten.

In Svelte 5 ersetzt man jeden davon durch einen kleinen Code-Schnipsel im Handler. Im Folgenden Schritt für Schritt.

preventDefault ersetzen

Der Klassiker — Form-Submit ohne Browser-Reload:

svelte Vorher (Svelte 4)
<form on:submit|preventDefault={save}>

</form>
svelte Nachher (Svelte 5)
<form onsubmit={(event) => { event.preventDefault(); save(); }}>

</form>

Was hier passiert:

  • event ist der Submit-Event, den der Browser uns übergibt.
  • event.preventDefault() sagt dem Browser: „Ja, ich kümmere mich selbst — bitte kein Reload.”
  • Danach rufen wir die eigene save()-Funktion auf.

Wenn die save-Funktion das Event nicht braucht, geht auch:

svelte Variante mit Auslagerung
<script>
    function handleSubmit(event) {
        event.preventDefault();
        save();
    }
</script>

<form onsubmit={handleSubmit}>…</form>

stopPropagation ersetzen

stopPropagation verhindert, dass der Event nach oben zu Eltern-Elementen wandert. Typischer Fall: Klicks innerhalb eines Modal-Dialogs sollen nicht den Hintergrund-Klick auslösen.

svelte Vorher (Svelte 4)
<div class="overlay" on:click={close}>
    <div class="dialog" on:click|stopPropagation>

    </div>
</div>
svelte Nachher (Svelte 5)
<div class="overlay" onclick={close}>
    <div class="dialog" onclick={(e) => e.stopPropagation()}>

    </div>
</div>

Die Logik bleibt: Der Klick auf den Dialog wird abgefangen, bevor er den Overlay-Listener erreicht. Das Modal schließt sich nur, wenn der Nutzer auf den Hintergrund klickt.

once ersetzen – Handler nur einmal

Bei once lief der Handler genau einmal, danach hat Svelte den Listener selbst entfernt. In Svelte 5 entfernst du den Listener nicht — du prüfst einen State-Wert und springst raus, wenn schon ausgeführt.

svelte Vorher (Svelte 4)
<button on:click|once={loadOnce}>Einmal laden</button>
svelte Nachher (Svelte 5)
<script>
    let alreadyDone = $state(false);

    function loadOnce() {
        if (alreadyDone) return;
        alreadyDone = true;

        // Eigentliche Logik
        fetch('/api/data');
    }
</script>

<button onclick={loadOnce}>Einmal laden</button>

Das ist nicht nur länger, sondern in den meisten Fällen auch klarer — du siehst, was passiert.

Alternativ ginge auch das Disabling des Buttons:

svelte Variante: Button disablen
<button onclick={loadOnce} disabled={alreadyDone}>
    {alreadyDone ? 'Bereits geladen' : 'Einmal laden'}
</button>

capture ersetzen

Events laufen normalerweise in zwei Phasen: erst runter durch den DOM-Baum (Capture-Phase), dann wieder rauf (Bubble-Phase). Standardmäßig hört dein Handler in der Bubble-Phase. Mit capture hörst du in der Capture-Phase — du bekommst das Event also bevor es weiter unten verarbeitet wird.

In Svelte 5 hängst du capture einfach an den Attribut-Namen an:

svelte Vorher (Svelte 4)
<div on:click|capture={onCapture}>…</div>
svelte Nachher (Svelte 5)
<div onclickcapture={onCapture}>…</div>

Das gleiche Prinzip gilt für jedes andere Event: onkeydowncapture, onfocusincapture und so weiter.

passive ersetzen

passive ist eine Performance-Optimierung für Scroll-Listener: Du sagst dem Browser „ich werde nicht preventDefault() aufrufen”, woraufhin der Browser nicht warten muss und flüssiger scrollt.

In Svelte 5 schreibt man das nicht im Markup — sondern fügt den Listener manuell per addEventListener hinzu, weil dort der dritte Parameter { passive: true } bekannt ist:

svelte passive-Listener manuell
<script>
    let element;

    $effect(() => {
        if (!element) return;

        function handle(event) {
            // ...kein event.preventDefault()
        }

        element.addEventListener('touchstart', handle, { passive: true });
        return () => element.removeEventListener('touchstart', handle);
    });
</script>

<div bind:this={element}>…</div>

In den allermeisten Fällen brauchst du passive aber gar nicht selbst — der Browser macht es automatisch für touchstart, touchmove und wheel-Listener.

self ersetzen – nur direkt ausgelöste Events

self ließ den Handler nur feuern, wenn das Event genau auf diesem Element ausgelöst wurde — nicht durch ein Kind, das es hochbubbelte. Praktisch z. B. bei Overlay-Klicks:

svelte Vorher (Svelte 4)
<div class="overlay" on:click|self={close}>
    <div class="dialog">…</div>
</div>
svelte Nachher (Svelte 5)
<div
    class="overlay"
    onclick={(event) => {
        if (event.target === event.currentTarget) close();
    }}
>
    <div class="dialog">…</div>
</div>

Erklärung der zwei Targets:

  • event.target — das Element, auf dem der Klick tatsächlich stattgefunden hat (kann ein Kind sein).
  • event.currentTarget — das Element, an dem dein Handler hängt.

Wenn beide identisch sind, war der Klick direkt auf dem Overlay — nicht auf dem Dialog darin. Dann schließen wir.

Wiederverwendbarer Helper für preventDefault

Wenn du oft preventDefault schreibst, lohnt sich ein kleiner Helper:

ts src/lib/event-helpers.ts
export function preventDefault<E extends Event>(handler: (event: E) => void) {
    return (event: E) => {
        event.preventDefault();
        handler(event);
    };
}
svelte Verwendung
<script>
    import { preventDefault } from '$lib/event-helpers';

    function save() {
        /* … */
    }
</script>

<form onsubmit={preventDefault(save)}>

</form>

Der Code wird wieder so kurz wie mit dem alten Modifier — nur eben als ganz normale TypeScript-Funktion, die du selbst geschrieben hast.

Vorher/Nachher auf einen Blick

text Migrations-Tabelle
Svelte 4                                Svelte 5
--------                                --------
on:click|preventDefault                 onclick={(e) => { e.preventDefault(); … }}
on:click|stopPropagation                onclick={(e) => e.stopPropagation()}
on:click|once                           onclick mit eigenem State-Flag
on:click|capture                        onclickcapture
on:click|self                           Vergleich event.target === event.currentTarget
on:click|passive                        addEventListener mit { passive: true }

Häufige Stolperfallen

preventDefault() nach await. event.preventDefault() muss synchron im Handler aufgerufen werden — nicht erst nach einem await. Sonst hat der Browser die Default-Aktion schon ausgeführt.

stopPropagation und preventDefault verwechselt.

  • preventDefault = „Browser, mach nicht den Default-Effekt.”
  • stopPropagation = „Event, geh nicht weiter zu meinen Eltern.” Beides sind unterschiedliche Dinge — manchmal braucht man beides, oft nur eines.

oncapture statt onclickcapture. Das capture hängt direkt an den Event-Namen — kein eigenes Attribut. Also onclickcapture, onkeydowncapture, etc.

once mit globalem Counter. let count = 0; if (count > 0) return; count++; außerhalb von $state ist nicht reaktiv — funktioniert zwar, aber DevTools zeigen den Wert nicht. Lieber let alreadyDone = $state(false);.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu Events

Zur Übersicht