transition: animiert das Erscheinen und Verschwinden von Elementen. Aber was, wenn ein Element bleibt und nur seine Position wechselt — etwa weil eine Liste neu sortiert wird? Dafür gibt es die zweite Direktive: animate:. Mit dem eingebauten Helfer flip aus svelte/animate werden Positionswechsel in Listen automatisch sanft animiert. Dieser Artikel erklärt das Konzept und zeigt es an einem Drag-and-Drop-artigen Beispiel.

Wann braucht man animate:?

Klassisches Szenario: Eine Liste, in der ein Eintrag nach oben verschoben wird.

svelte Ohne Animation
<script>
    let items = $state([
        { id: 1, label: 'Apfel' },
        { id: 2, label: 'Banane' },
        { id: 3, label: 'Kirsche' },
    ]);

    function moveUp(index) {
        if (index === 0) return;
        [items[index - 1], items[index]] = [items[index], items[index - 1]];
    }
</script>

<ul>
    {#each items as item, i (item.id)}
        <li>
            {item.label}
            <button onclick={() => moveUp(i)}>↑</button>
        </li>
    {/each}
</ul>

Beim Klick auf springt der Eintrag augenblicklich an seine neue Position. Es gibt keinen visuellen Hinweis darauf, was sich verschoben hat — der Nutzer muss aktiv vergleichen.

Mit animate:flip

Mit einer einzigen zusätzlichen Direktive wird daraus eine sanfte Animation:

svelte Mit flip
<script>
    import { flip } from 'svelte/animate';

    let items = $state([
        { id: 1, label: 'Apfel' },
        { id: 2, label: 'Banane' },
        { id: 3, label: 'Kirsche' },
    ]);

    function moveUp(index) {
        if (index === 0) return;
        [items[index - 1], items[index]] = [items[index], items[index - 1]];
    }
</script>

<ul>
    {#each items as item, i (item.id)}
        <li animate:flip={{ duration: 300 }}>
            {item.label}
            <button onclick={() => moveUp(i)}>↑</button>
        </li>
    {/each}
</ul>

Die <li>-Elemente gleiten jetzt sanft an ihre neue Position. Der Nutzer sieht, was sich bewegt hat — die Liste fühlt sich plötzlich physisch und nachvollziehbar an.

Voraussetzungen

animate:flip funktioniert nur, wenn:

  1. Das Element in einem {#each}-Block liegt.
  2. Der {#each}-Block einen Key-Ausdruck hat — also {#each items as item (item.id)}. Ohne Key kann Svelte nicht zuordnen, welches Element wohin gewandert ist.
  3. Die Animation läuft, weil sich die Reihenfolge der Items ändert. Bei einer kompletten Neu-Liste mit anderen IDs würde Svelte alte unmounten und neue mounten — keine Animation.

Wenn eine dieser Bedingungen nicht erfüllt ist, passiert nichts oder du bekommst eine Warnung.

Wie funktioniert das? Das FLIP-Prinzip

Der Name flip kommt von einer Animations-Technik mit demselben Namen — kurz für First, Last, Invert, Play. Das ist auch das Geheimnis der Implementierung:

  1. First — Vor der Aktualisierung wird die Position jedes Elements gemessen.
  2. Last — Nach der Aktualisierung wird die neue Position gemessen.
  3. Invert — Über CSS-Transform wird das Element scheinbar an seinen alten Platz gesetzt (Differenz aus Last und First).
  4. Play — Das Transform wird sanft auf 0 animiert. Das Element gleitet vom alten zum neuen Platz.

Das Ergebnis ist eine sehr glatte Animation, weil der Browser nur einen Compositor-CSS-Transform animiert — nicht das ganze Layout neu berechnet.

Parameter

flip akzeptiert die üblichen Animations-Parameter:

ParameterBeschreibung
delayWartezeit in Millisekunden
durationDauer in Millisekunden oder als Funktion (d) => ms
easingEasing-Funktion (Default: cubicOut)

Ein interessantes Detail: duration darf eine Funktion sein, die mit der gemessenen Distanz aufgerufen wird:

svelte Distanz-abhängige Dauer
<li animate:flip={{ duration: (d) => Math.sqrt(d) * 50 }}>
    {item.label}
</li>

Damit wird die Animation länger, wenn das Element weiter wandern muss. Das wirkt natürlicher als eine konstante Dauer.

Praxis-Beispiel: Sortierbare Liste

Eine Demo mit Drag-and-Drop-artiger Bedienung — hier vereinfacht über Buttons:

svelte SortableList.svelte
<script>
    import { flip } from 'svelte/animate';
    import { fade } from 'svelte/transition';

    let items = $state([
        { id: 1, label: 'Brot kaufen' },
        { id: 2, label: 'Spülmaschine ausräumen' },
        { id: 3, label: 'Pflanzen gießen' },
        { id: 4, label: 'Müll rausbringen' },
    ]);

    function move(index, delta) {
        const target = index + delta;
        if (target < 0 || target >= items.length) return;
        [items[index], items[target]] = [items[target], items[index]];
    }

    function remove(id) {
        items = items.filter((item) => item.id !== id);
    }
</script>

<ul>
    {#each items as item, i (item.id)}
        <li
            animate:flip={{ duration: 300 }}
            transition:fade={{ duration: 200 }}
        >
            <span>{item.label}</span>
            <button onclick={() => move(i, -1)} disabled={i === 0}>↑</button>
            <button onclick={() => move(i, 1)} disabled={i === items.length - 1}>↓</button>
            <button onclick={() => remove(item.id)}>×</button>
        </li>
    {/each}
</ul>

animate:flip und transition:fade ergänzen sich:

  • flip kümmert sich um Items, die ihren Platz wechseln.
  • fade kümmert sich um Items, die hinzukommen oder verschwinden.

Zusammen ergibt das eine richtig flüssige UX.

animate: ist nur für {#each}

Ein wichtiger Punkt: animate: funktioniert nur auf direkten Kindern eines {#each}-Blocks. Auf einem normalen <div> außerhalb von {#each} ist die Direktive nicht erlaubt.

svelte ❌ Falsch
<div animate:flip>

</div>
svelte + Richtig
{#each items as item (item.id)}
    <div animate:flip>{item.label}</div>
{/each}

Eigene animate:-Funktionen

Wie bei Transitions kannst du auch eigene Animate-Funktionen schreiben. Die Signatur unterscheidet sich leicht: Die Funktion bekommt zusätzlich ein rect-Objekt mit der gemessenen Distanz und gibt ein { duration, css }-Objekt zurück.

ts Eigene Animate-Funktion
export function flipWithBounce(node, { from, to }, params) {
    const dx = from.left - to.left;
    const dy = from.top - to.top;
    return {
        duration: params.duration ?? 400,
        css: (t, u) => `
            transform: translate(${u * dx}px, ${u * dy}px)
                       scale(${1 + Math.sin(t * Math.PI) * 0.1});
        `,
    };
}

from und to sind die DOMRect-Objekte vor und nach der Bewegung. Damit lässt sich jede gewünschte Choreographie bauen.

Häufige Stolperfallen

Kein Key im {#each}-Block. Ohne (item.id) kann Svelte nicht erkennen, welches Element wohin verschoben wurde. Animation läuft nicht oder falsch.

Index als Key bei Reorder. Wenn der Key der Index ist, ändert sich beim Sortieren der Key-Wert jedes Items. Svelte denkt: „neue Items” und mountet alles neu — keine Animation. Echte ID nutzen.

Animation fühlt sich „springt”. Standard-duration ist 400 ms. Bei sehr kurzen Distanzen wirkt das langsam. Mit der Funktions-Form von duration proportional zur Distanz machen.

Performance bei sehr langen Listen. Hunderte Items mit animate:flip können beim Reorder ruckeln. In dem Fall: nur sichtbare Items animieren (z. B. mit Virtual Scrolling) oder die Animation ganz weglassen.

Vergessenes transition: zusätzlich. flip animiert nur Position-Wechsel. Hinzukommende oder verschwindende Items brauchen separat transition: oder in:/out:.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu Transitions & Animations

Zur Übersicht