# Motion, Transitions und Animationen in Svelte

URL: https://www.mikebild.dev/de/blog/svelte-motion-transitions-animationen/

Ein Hinweis über der Liste taucht auf, und er ist einfach da – hart, ohne Übergang. Ein Eintrag wird gelöscht, und alles darunter rutscht schlagartig eine Zeile nach oben. In React-Projekten landet spätestens an dieser Stelle `react-transition-group` oder eine Animations-Runtime im `package.json`. In Svelte sind es drei Importe aus der Standardausstattung, und die Abhängigkeitsliste bleibt, wie sie war.

Die drei Module sind aufeinander abgestimmt und decken zusammen fast alles ab, was im Alltag anfällt. Wer schon einmal verstanden hat, warum [Svelte ohne Virtual DOM auskommt und stattdessen zur Compile-Zeit Code erzeugt](https://www.mikebild.dev/de/blog/svelte-reaktivitaet-compiler-ohne-virtual-dom/), 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.

**Redaktionelle Einordnung (Juli 2026):** Die Beispiele sind im Svelte-3-Stil geschrieben. `svelte/transition`, `svelte/animate` und `svelte/motion` gibt es auch in Svelte 5; geändert hat sich vor allem das umgebende Reaktivitätsmodell. Die alten Tutorial-URLs wurden durch die heutigen API-Seiten ersetzt.

## 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, die mir begegnet sind, entstehen daraus, dass jemand das falsche Werkzeug für die falsche Aufgabe greift.

- `transition:` (aus `svelte/transition`) beschreibt, wie ein einzelnes Element in den DOM hinein- und wieder herauskommt. Es geht um Erscheinen und Verschwinden.
- `animate:` (aus `svelte/animate`) beschreibt, wie sich Elemente bewegen, die im DOM bleiben, aber ihre Position ändern – also beim Umsortieren einer Liste.
- `svelte/motion` (mit `tweened` und `spring`) 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.

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

```svelte
<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 mir selbst erst beim genauen Hinsehen klar geworden ist: 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.

```mermaid
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:flip 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:

```svelte
<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](https://www.mikebild.dev/de/blog/svelte-stores-zustand-ausserhalb-der-komponente/) 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.

```svelte
<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.

```svelte
<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 Details fallen erst beim Arbeiten damit auf. 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 `await`en. 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. Das ist der schnellste Weg, ein Gefühl dafür zu bekommen, was die beiden Zahlen tatsächlich bewirken – schneller jedenfalls, als die Werte im Code zu raten und neu zu laden.

## 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.

```js
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:

```svelte
<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` / `spring` aus `svelte/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 Projektalltag 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

- Foliensatz zu diesem Thema: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-svelte.html
- Repository zur Artikelreihe mit den lauffähigen Svelte-Beispielen
- [`svelte/transition` – offizielle Dokumentation](https://svelte.dev/docs/svelte/svelte-transition)
- [`svelte/animate` – offizielle Dokumentation](https://svelte.dev/docs/svelte/svelte-animate)
- [`svelte/motion` – offizielle Dokumentation](https://svelte.dev/docs/svelte/svelte-motion)
