Motion, Transitions und Animationen in Svelte
Wie Svelte Bewegung deklarativ und ohne Extra-Bibliothek mitbringt: transition-Direktiven fürs Ein- und Ausblenden, svelte/motion für sanft interpolierte Werte und animate:flip fürs Umsortieren von Listen.
In fast jedem Frontend-Workshop kommt irgendwann die Frage: „Und wie machen wir das jetzt hübsch beweglich?" Gemeint sind dann meist die kleinen Dinge – ein Element, das beim Einblenden sanft hereinschwebt, eine Liste, die sich nach dem Sortieren nicht ruckartig neu aufbaut, ein Zahlenwert, der nicht springt, sondern gleitet. In React-Projekten folgt darauf oft der Griff zur nächsten Bibliothek. In Svelte ist meine Antwort meistens kürzer: Das ist schon da. Bewegung gehört bei Svelte zum Sprachumfang, nicht zum Ökosystem.
Genau das möchte ich in diesem Artikel zeigen. Svelte bringt drei aufeinander abgestimmte Bausteine mit, die zusammen fast alles abdecken, was man im Alltag braucht – ohne dass man react-transition-group oder eine Animations-Runtime dazuholen muss. Wer schon einmal verstanden hat, warum Svelte ohne Virtual DOM auskommt und stattdessen zur Compile-Zeit Code erzeugt, ahnt bereits, worauf das hinausläuft: Auch die Bewegung ist eng mit dem Compiler verwoben – er übersetzt die Direktiven in Code, der die Animation nach Möglichkeit über CSS-Keyframes statt über JavaScript pro Frame laufen lässt.
Drei Werkzeuge, drei Aufgaben
Bevor wir Code schreiben, lohnt sich eine saubere Trennung im Kopf. Die drei Module haben unterschiedliche Zuständigkeiten, und die meisten Missverständnisse in Schulungen entstehen daraus, dass jemand das falsche Werkzeug für die falsche Aufgabe greift.
transition:(aussvelte/transition) beschreibt, wie ein einzelnes Element in den DOM hinein- und wieder herauskommt. Es geht um Erscheinen und Verschwinden.animate:(aussvelte/animate) beschreibt, wie sich Elemente bewegen, die im DOM bleiben, aber ihre Position ändern – also beim Umsortieren einer Liste.svelte/motion(mittweenedundspring) hat mit dem DOM erst einmal gar nichts zu tun. Hier geht es um Werte, die sich weich von einem Zustand zum nächsten interpolieren – eine Zahl, eine Farbe, eine Koordinate.
Diese Dreiteilung ist der rote Faden für alles Weitere. Sobald klar ist, ob man ein Element auftauchen lässt, es innerhalb einer Liste verschiebt oder einen Wert gleiten lässt, ergibt sich das passende Modul fast von selbst.
Transitions: Ein- und Ausblenden
Eine Transition hängt man in Svelte als Direktive an ein Element. Sie wird ausgelöst, sobald das Element durch einen Kontrollfluss – ein {#if}, ein {#each} – in den DOM eintritt oder ihn verlässt. Das ist der entscheidende Punkt: Nicht ich rufe eine Animation auf, sondern die Zustandsänderung tut es. Ich beschreibe nur, wie sie aussieht.
<script>
import { fade, fly } from 'svelte/transition';
let visible = true;
</script>
<label>
<input type="checkbox" bind:checked={visible} />
toggle
</label>
{#if visible}
<p transition:fade={{ duration: 300 }}>
fades in and out
</p>
<div transition:fly={{ y: 20, duration: 300 }}>
flies in from below, flies back down on exit
</div>
{/if}
transition: gilt bidirektional – dieselbe Bewegung läuft beim Einblenden vorwärts und beim Ausblenden rückwärts. Das ist häufig genau richtig. Manchmal aber möchte man asymmetrisch sein: hereinfliegen, aber verblassend verschwinden. Dafür gibt es die getrennten Direktiven in: und out:.
<script>
import { fly, fade } from 'svelte/transition';
let visible = true;
</script>
{#if visible}
<div in:fly={{ y: 20, duration: 300 }} out:fade={{ duration: 200 }}>
enters flying, leaves fading
</div>
{/if}
Das eingebaute Sortiment ist überschaubar und gut aufeinander abgestimmt: fade, blur, fly, slide, scale, draw und crossfade. Für den Alltag reichen die ersten fünf fast immer. fly nimmt x und y als Zahlen in Pixeln entgegen, slide blendet vertikal auf und zu, scale skaliert von einem Startwert her.
Ein Detail, das in Schulungen regelmäßig für Aha-Momente sorgt: Eine Out-Transition braucht Zeit, und das Element bleibt für diese Dauer im DOM. Svelte entfernt es erst, wenn die Bewegung abgeschlossen ist. Wer also glaubt, ein Element sei schon weg, während es noch ausblendet, wundert sich über kurzzeitig doppelte Einträge. Das ist kein Fehler, sondern die Voraussetzung dafür, dass man das Verschwinden überhaupt sehen kann.
Was der Compiler intern daraus macht
Der Grund, warum diese Transitions ohne Performance-Sorgen laufen, liegt darin, wie sie erzeugt werden. Eine Transition-Funktion liefert im Kern eine css-Funktion zurück, die für einen Fortschrittswert t (von 0 bis 1) einen CSS-String erzeugt. Aus dieser Funktion baut Svelte echte CSS-Keyframes. Die Animation läuft damit über die CSS-Engine des Browsers und blockiert nicht den Main-Thread mit JavaScript pro Frame.
flowchart LR
A["State change<br/>{#if} / {#each}"] --> B["Svelte compiler"]
B --> C["transition: / animate:<br/>directive"]
C --> D{"css function<br/>available?"}
D -->|yes| E["CSS keyframes<br/>off main thread"]
D -->|no| F["tick per frame<br/>JS on main thread"]
E --> G["DOM update"]
F --> G
Nur wenn eine Bewegung sich nicht rein über CSS ausdrücken lässt – etwa weil man pro Frame einen Textinhalt ändern will –, greift der zweite Weg über eine tick-Funktion, die tatsächlich pro Frame in JavaScript läuft. Der Standardfall aber ist der performante CSS-Pfad, und man bekommt ihn geschenkt, ohne selbst Keyframes zu schreiben.
animate für Listen
Kommen wir zum zweiten Werkzeug. transition: kümmert sich um Elemente, die kommen und gehen. Aber was ist mit Elementen, die bleiben und nur ihren Platz tauschen? Wenn ich eine Liste sortiere oder mische, springen die Einträge ohne Zutun hart an ihre neue Position. Genau hier setzt animate:flip an.
Der Name FLIP steht für „First, Last, Invert, Play" – eine bekannte Technik, bei der die alte und die neue Position eines Elements verglichen und die Differenz weganimiert wird. Svelte erledigt diese Rechnung für uns. Wichtig ist nur eine Bedingung: animate: funktioniert ausschließlich in einem keyed {#each}-Block, also einem {#each} mit einem stabilen Schlüssel. Ohne diesen Schlüssel kann Svelte gar nicht wissen, welches Element vorher wo war.
Das folgende Beispiel bringt beide Werkzeuge zusammen – transition:fly fürs Ein- und Ausblenden neuer beziehungsweise entfallender Einträge und animate:flip fürs weiche Umsortieren der bleibenden:
<script>
import { fly } from 'svelte/transition';
import { flip } from 'svelte/animate';
let items = [
{ id: 1, label: 'apples' },
{ id: 2, label: 'oranges' },
{ id: 3, label: 'bananas' },
{ id: 4, label: 'pears' }
];
function shuffle() {
items = items
.map((item) => ({ item, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ item }) => item);
}
function remove(id) {
items = items.filter((item) => item.id !== id);
}
</script>
<button on:click={shuffle}>shuffle</button>
<ul>
{#each items as item (item.id)}
<li
transition:fly={{ y: 20, duration: 300 }}
animate:flip={{ duration: 300 }}
>
{item.label}
<button on:click={() => remove(item.id)}>x</button>
</li>
{/each}
</ul>
Der Schlüssel (item.id) ist hier nicht dekorativ, sondern trägt die ganze Mechanik. Er erlaubt es Svelte, dieselbe DOM-<li> über das Umsortieren hinweg als dasselbe Element zu erkennen und die Positionsdifferenz zu animieren. Das animierte Element muss außerdem direktes Kind des {#each}-Blocks sein. Wer stattdessen einen Wrapper dazwischenschiebt und animate:flip an das falsche Element hängt, sieht schlicht keine Bewegung – ein Klassiker, über den ich in Reviews immer wieder stolpere.
Das Zusammenspiel von transition und animate in derselben Liste ist übrigens Absicht und gut abgestimmt: Neue Einträge fliegen herein, entfernte fliegen heraus und bleiben dafür kurz im DOM, und alle übrigen rutschen sanft auf ihre neuen Plätze. Drei Bewegungen, ein deklarativer Block.
svelte/motion: Werte, die gleiten
Das dritte Werkzeug lässt den DOM zunächst außen vor. tweened und spring sind Stores – schreibbare Stores, wie ich sie im Artikel über Zustand außerhalb der Komponente mit Svelte Stores beschrieben habe, nur mit einer Besonderheit: Wenn man ihren Wert setzt, springen sie nicht sofort auf das Ziel, sondern interpolieren dorthin. Man liest sie im Markup wie jeden Store mit dem $-Präfix.
tweened interpoliert über eine feste Dauer und eine Easing-Funktion – gut für alles, was einem klaren zeitlichen Verlauf folgen soll, etwa ein Fortschrittsbalken.
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const progress = tweened(0, {
duration: 400,
easing: cubicOut
});
</script>
<progress value={$progress}></progress>
<button on:click={() => progress.set(0)}>0%</button>
<button on:click={() => progress.set(0.5)}>50%</button>
<button on:click={() => progress.set(1)}>100%</button>
spring dagegen interpoliert nicht über eine Dauer, sondern über eine Physik – Steifigkeit und Dämpfung. Das Ergebnis wirkt lebendiger, weil es leicht nachfedert, statt exakt zu einem Zeitpunkt anzukommen. Für Dinge, die sich „natürlich" anfühlen sollen, etwa ein Element, das der Maus folgt oder auf Eingaben reagiert, ist spring meist die schönere Wahl.
<script>
import { spring } from 'svelte/motion';
const size = spring(1, {
stiffness: 0.1,
damping: 0.4
});
</script>
<div class="box" style="transform: scale({$size})">
{$size.toFixed(2)}
</div>
<button on:click={() => size.set(0.5)}>small</button>
<button on:click={() => size.set(1)}>normal</button>
<button on:click={() => size.set(2)}>large</button>
<style>
.box {
width: 80px;
height: 80px;
background: #ff3e00;
display: grid;
place-items: center;
color: white;
}
</style>
Zwei Dinge sind hier wichtig zu wissen. Erstens geben .set() und .update() ein Promise zurück, das erst auflöst, wenn die Interpolation abgeschlossen ist – wer also darauf warten will, dass ein Wert „angekommen" ist, kann das awaiten. Zweitens interpoliert Svelte Zahlen, Arrays und Objekte aus Zahlen automatisch; für andere Werttypen, etwa Farben in einer bestimmten Notation, gibt man tweened über die Option interpolate eine eigene Interpolationsfunktion mit.
Die Parameter stiffness und damping lassen sich sogar zur Laufzeit anpassen. Man kann sie an Slider binden und live erleben, wie sich das Verhalten der Feder ändert – im Workshop ist das immer der Moment, in dem es bei den meisten klick macht, was diese beiden Zahlen eigentlich bedeuten.
Eigene Transitions schreiben
Der vielleicht schönste Beweis dafür, dass hier keine Blackbox arbeitet: Eine Transition ist nichts weiter als eine Funktion. Sie bekommt den DOM-Knoten und ihre Parameter und gibt ein Objekt zurück, das unter anderem eine css-Funktion enthält. Diese Funktion erhält den Fortschritt t von 0 bis 1 sowie den Gegenwert u, der 1 - t entspricht, und liefert einen CSS-String.
function spin(node, { duration = 400 }) {
return {
duration,
css: (t, u) => `
transform: rotate(${u * 360}deg) scale(${t});
opacity: ${t};
`
};
}
Verwenden lässt sich diese eigene Transition exakt wie die eingebauten:
<script>
import { spin } from './spin.js';
let visible = true;
</script>
{#if visible}
<div in:spin={{ duration: 600 }}>
spins into view
</div>
{/if}
Wenn man das einmal geschrieben hat, entzaubert sich der Rest von selbst. fade, fly und scale sind genau solche Funktionen – nur eben in der Bibliothek. Es gibt keinen zweiten, verborgenen Mechanismus. Genau diese Durchgängigkeit ist es, die ich an Sveltes Bewegungsmodell so schätze: Vom fertigen Einzeiler bis zur eigenen Funktion ist es ein durchgehendes Konzept, kein Bruch.
Zur Einordnung noch einmal die drei Werkzeuge im direkten Vergleich, damit die Zuordnung dauerhaft sitzt:
transition:/in:/out:– für das Erscheinen und Verschwinden einzelner Elemente, ausgelöst durch{#if}oder{#each}.animate:flip– für Elemente, die im DOM bleiben und in einem keyed{#each}nur ihre Position tauschen.tweened/springaussvelte/motion– für interpolierte Werte, die man als Store mit$liest und ins Markup einbindet.
Fazit
Bewegung ist in Svelte keine Zusatzentscheidung, die man später mit einer Bibliothek nachrüstet, sondern ein Teil der Sprache. Das verändert die Art, wie man an das Thema herangeht. Statt zu fragen „Welches Paket nehme ich?", fragt man „Erscheint hier etwas, verschiebt sich etwas, oder gleitet ein Wert?" – und die Antwort führt direkt zu transition:, animate:flip oder svelte/motion.
Der gemeinsame Nenner ist der deklarative Ansatz: Ich beschreibe, wie eine Bewegung aussieht, und der Compiler kümmert sich darum, dass sie ausgelöst wird, wenn sich der Zustand ändert, und dass sie möglichst über CSS-Keyframes performant läuft. Dass eigene Transitions dieselben schlichten Funktionen sind wie die eingebauten, macht das Ganze lernbar bis auf den Grund. Für den Workshop-Alltag heißt das: weniger Abhängigkeiten, weniger Boilerplate und mehr Zeit für die Frage, welche Bewegung eine Oberfläche wirklich verständlicher macht – statt für die Frage, welches Framework sie überhaupt ermöglicht.
Weiterführende Quellen
- Repository zur Artikelreihe mit den lauffähigen Svelte-Beispielen
- svelte/transition – offizielle Dokumentation
- svelte/motion – offizielle Dokumentation
- Svelte-Tutorial: Tweened, Spring, Animate und Custom CSS Transitions
Kommentare