Die eingebauten Transitions decken die meisten Standard-Bedürfnisse ab. Wenn du etwas Eigenes brauchst — eine Schreibmaschinen-Animation, einen Farbwechsel, eine Kombination aus mehreren Effekten — schreibst du dir eine Custom Transition. Das ist einfacher, als es klingt: Eine Transition ist im Grunde nur eine Funktion mit einer bestimmten Signatur. Dieser Artikel zeigt beide Wege (CSS-basiert und JavaScript-basiert) mit verständlichen Beispielen.
Die Signatur einer Custom Transition
Eine Transition ist eine Funktion, die zwei Argumente bekommt:
- Den DOM-Knoten (das Element, auf das sie angewendet wird).
- Ein Parameter-Objekt (das, was du in
transition:fn={...}mitgibst).
Sie gibt ein Objekt zurück, das beschreibt, wie die Animation aussieht. Es gibt zwei Varianten — css und tick.
function myTransition(node, params) {
return {
delay: 0,
duration: 400,
easing: (t) => t,
css: (t, u) => `…`, // Variante 1
tick: (t, u) => { … }, // Variante 2
};
}Erklärung der t/u-Parameter:
tgeht von0bis1während der Animation (Fortschritt).uist1 - t(Restweg).- Bei einer **Out-**Transition geht
tvon1runter auf0(umgekehrte Richtung).
Das ist alles, was Svelte braucht. Den Rest macht die Engine.
Variante 1: css — performant via CSS-Strings
Die css-Variante ist die bevorzugte Form. Du gibst einen CSS-String zurück, Svelte erzeugt daraus eine echte CSS-Keyframe-Animation. Die läuft im Browser-Compositor — also off-thread und sehr performant.
export function spin(node, { duration = 400 } = {}) {
return {
duration,
css: (t) => {
const eased = t;
return `
transform: scale(${eased}) rotate(${eased * 360}deg);
opacity: ${eased};
`;
},
};
}<script>
import { spin } from '$lib/transitions/spin';
let visible = $state(true);
</script>
{#if visible}
<div transition:spin>Spin!</div>
{/if}Die Funktion css(t, u) wird intern in eine CSS-Keyframe-Animation übersetzt. Das ist deutlich schneller als JavaScript-basierte Animation per Frame, weil der Browser den Kompositor nutzen kann.
Variante 2: tick — für DOM-Manipulation pro Frame
Wenn du etwas animieren willst, was sich nicht über CSS abbilden lässt — etwa den Inhalt eines Elements verändern, Canvas-Zeichnen oder DOM-Attribute setzen — gibt es tick. Diese Funktion läuft pro Animations-Frame.
export function typewriter(node, { speed = 50 } = {}) {
const text = node.textContent;
const duration = text.length * speed;
return {
duration,
tick: (t) => {
const i = Math.floor(text.length * t);
node.textContent = text.slice(0, i);
},
};
}<script>
import { typewriter } from '$lib/transitions/typewriter';
let visible = $state(true);
</script>
{#if visible}
<p in:typewriter>Hallo, ich werde getippt.</p>
{/if}Was hier passiert:
- Beim Start liest die Funktion den vollständigen Text aus dem DOM-Knoten.
- Bei jedem Frame berechnet sie, wie viel des Textes schon „getippt” sein sollte (
t * length). - Sie schreibt den Substring zurück ins DOM.
CSS könnte das nicht — es kann keinen Textinhalt verändern. tick ist hier zwingend.
Beide Varianten kombinieren
Du darfst css und tick zusammen zurückgeben. CSS macht das Optische, tick setzt zum Schluss noch DOM-Attribute auf. Das wird selten gebraucht, ist aber möglich.
Praxis-Beispiel: Background-Color-Fade
Eine Transition, die nicht nur Opazität fadet, sondern auch die Hintergrundfarbe von neutral auf Akzent wechselt — etwa zum Hervorheben einer neu eingegangenen Nachricht.
export function highlight(node, { color = '#fff7c2', duration = 800 } = {}) {
const original = getComputedStyle(node).backgroundColor;
return {
duration,
css: (t, u) => `
background-color: color-mix(in srgb, ${color} ${u * 100}%, ${original});
`,
};
}<script>
import { highlight } from '$lib/transitions/highlight';
let messages = $state([]);
</script>
<ul>
{#each messages as msg (msg.id)}
<li in:highlight>{msg.text}</li>
{/each}
</ul>u ist 1 - t, läuft also von 1 runter auf 0. So beginnen wir mit dem Highlight-Color und faden schrittweise zum Original-Hintergrund.
Easing-Funktionen einsetzen
Die optionale easing-Funktion mappt t (linear 0→1) auf einen anderen Verlauf — z. B. „erst langsam, dann schneller”. Standardmäßig ist easing linear.
import { cubicInOut } from 'svelte/easing';
export function spin(node, { duration = 400 } = {}) {
return {
duration,
easing: cubicInOut,
css: (t) => `transform: rotate(${t * 360}deg)`,
};
}Wer easing setzt, bekommt das gemappte t in der css/tick-Funktion. Aus t = 0.5 wird so vielleicht 0.62 — ein realistischeres Bewegungsgefühl.
Asynchrones Setup: Funktion gibt Funktion zurück
Manche Transitions brauchen Setup-Zeit vor der eigentlichen Animation — etwa um eine Größe zu messen oder ein Bild zu laden. Dafür gibst du eine Funktion zurück, die ihrerseits das Transition-Objekt zurückgibt:
export function expand(node) {
const width = node.scrollWidth;
return () => ({
duration: 400,
css: (t) => `width: ${t * width}px`,
});
}Svelte ruft die äußere Funktion direkt beim Mount auf — du kannst Werte messen, bevor die Animation startet. Die innere Funktion wird kurz vor der Animation aufgerufen — ideal für asynchrones Setup.
Das brauchst du selten, ist aber gut zu kennen.
Häufige Stolperfallen
css und tick zur Laufzeit gewechselt.
Du gibst beim Setup zurück, was du nutzen willst. Während der Animation lässt sich nicht zwischen den Varianten wechseln.
duration als Pflicht-Property vergessen.
Ohne duration weiß Svelte nicht, wie lange die Animation laufen soll. Default-Wert setzen oder Pflicht-Parameter erzwingen.
tick statt css, obwohl CSS reicht.
tick ist langsamer, weil JavaScript jeden Frame ausgeführt wird. Wenn der Effekt mit Transform/Opacity/Filter darstellbar ist, immer css nehmen.
Element-Position innerhalb von tick falsch.
Wer in tick getBoundingClientRect() aufruft und dann das DOM ändert, riskiert Layout-Thrashing. Solche Operationen lieber im Setup-Phase machen, nicht pro Frame.
Out-Transition mit t vorwärts erwartet.
Bei einer Out-Animation läuft t rückwärts (von 1 auf 0). Wenn deine Berechnung das nicht berücksichtigt, sieht die Animation rückwärts seltsam aus. Im Zweifel mit t und u testen, was wann gilt.