svelte
svelte copied to clipboard
Svelte 5 flip behavior
Describe the bug
After updating my project from svelte 4 to 5 ("5.0.0-next.264"), the behavior for svelte's animate:flip action has changed subtly. I'm using the built in animation and transition actions in tandem with @neodrag/svelte but I think the issue is in the way svelte 5 reconciles changes to {#each} blocks.
What I expect to happen is the flip animation plays after I release the element I'm dragging. In svelte 5, the flip animation doesn't trigger and it abruptly snaps back to its start position.
Reproduction
Minimal reproduction: https://github.com/jessecoleman/svelte-5-flip
v4: https://github.com/user-attachments/assets/316cc2df-bdc3-4b0d-ac1f-76778aaad8de
v5: https://github.com/user-attachments/assets/a5f72ed8-b146-43fd-9d08-12f02c78f22f
Logs
No response
System Info
System:
OS: Linux 5.15 Ubuntu 22.04.3 LTS 22.04.3 LTS (Jammy Jellyfish)
CPU: (22) x64 Intel(R) Core(TM) Ultra 7 155H
Memory: 12.14 GB / 15.31 GB
Container: Yes
Shell: 5.8.1 - /usr/bin/zsh
Binaries:
Node: 20.11.1 - ~/.nvm/versions/node/v20.11.1/bin/node
npm: 10.2.4 - ~/.nvm/versions/node/v20.11.1/bin/npm
pnpm: 8.15.5 - ~/.local/share/pnpm/pnpm
npmPackages:
svelte: 5.0.0-next.264 => 5.0.0-next.264
Severity
annoyance
As per docs, it shouldn't work (unless I misunderstood something):
An animation is triggered when the contents of a keyed each block are re-ordered. Animations do not run when an element is added or removed, only when the index of an existing data item within the each block changes. Animate directives must be on an element that is an immediate child of a keyed each block.
But if you swap items, animation will work:
<svelte:options runes={true} />
<script>
import { draggable } from "@neodrag/svelte";
import { flip } from "svelte/animate";
let list = $state([0, 1, 2, 3]);
let dragId = $state(-1);
let dragPos = $state([0, 0]);
const handleDragStart = (e, id) => {
dragPos = [e.detail.offsetX, e.detail.offsetY];
dragId = id;
};
const handleDrag = (e) => {
dragPos = [e.detail.offsetX, e.detail.offsetY];
};
const handleDragEnd = (e) => {
// NOTE: this swapping triggers animate
let item = list[0]
list[0] = list[1]
list[1] = item
dragPos = [0, 0];
dragId = -1;
};
</script>
<div class="list">
{#each list as li (li)}
<div
class="list-item"
style="
transform: translate3d(0px, {40 * li}px);
"
animate:flip
use:draggable={{
position:
dragId === li
? { x: dragPos[0], y: dragPos[1] }
: { x: 0, y: 40 * li },
}}
on:neodrag:start={(e) => handleDragStart(e, li)}
on:neodrag={handleDrag}
on:neodrag:end={handleDragEnd}
>
{li}
</div>
{/each}
</div>
<style>
.list {
position: relative;
}
.list-item {
position: absolute;
width: 35px;
height: 35px;
background: red;
}
</style>
PS: The code from the repo does work in Svelte 4 (one will have to downgrade to 4.2.19, to make it work). Was it a bug or a feature?
Yeah, the swap behavior is working for me, I mostly wanted to avoid having to hack together some imperative animation to achieve the "return to origin" effect that was default in Svelte 4. I'm guessing there are some performance optimizations in Svelte 5 that take advantage of the list order remaining the same. Is there a best practice for firing off an animation imperatively in Svelte (maybe using WAAPI?) I already have a reference to the DOM element in the dragend event.
BTW, here's the full non-toy sample of the application where I'm relying on this behavior: https://gramjam.app/classic
I was able to work around this by conditionally adding a css class with transition: transform 0.5s ease to the element being dragged after releasing. It works well enough, although it creates a bit more complexity in my application code. Ok with closing this one for now.