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 |:
| Modifier | Zweck |
|---|---|
preventDefault | Browser-Standardverhalten unterdrücken (z. B. Form-Submit-Reload). |
stopPropagation | Event nicht weiter an Eltern-Elemente bubblen. |
stopImmediatePropagation | Auch andere Listener auf demselben Element nicht aufrufen. |
once | Handler nur einmal ausführen, dann automatisch entfernen. |
capture | Im Capture-Phase laufen statt im Bubble-Phase. |
passive | Browser darf nicht auf preventDefault() warten (für Scroll-Performance). |
nonpassive | Gegenteil: explizit aktiv (überschreibt automatische Defaults). |
self | Nur reagieren, wenn das Event direkt am Element ausgelöst wurde. |
trusted | Nur 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:
<form on:submit|preventDefault={save}>
…
</form><form onsubmit={(event) => { event.preventDefault(); save(); }}>
…
</form>Was hier passiert:
eventist 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:
<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.
<div class="overlay" on:click={close}>
<div class="dialog" on:click|stopPropagation>
…
</div>
</div><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.
<button on:click|once={loadOnce}>Einmal laden</button><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:
<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:
<div on:click|capture={onCapture}>…</div><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:
<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:
<div class="overlay" on:click|self={close}>
<div class="dialog">…</div>
</div><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:
export function preventDefault<E extends Event>(handler: (event: E) => void) {
return (event: E) => {
event.preventDefault();
handler(event);
};
}<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
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);.