[Svelte] DragOverlay disappears one frame before the hidden source is restored
When the source content is hidden using draggable.isDragSource, the default drop animation ends with a brief blank frame: the overlay disappears before the source becomes visible again.
The source element is already in the DOM and remains connected throughout the animation and cleanup. This reproduces with a single draggable, without Query, network requests, list updates, or application state changes.
Environment
@dnd-kit/svelte: 0.5.0@dnd-kit/dom: 0.5.0- Svelte: 5.57.0
- Chromium: 151.0.7922.34, via Playwright 1.62.1
- Windows
- Default drop animation; normal motion preference
Minimal reproduction
In a Svelte/Vite project with the versions above, use these two components. The original card keeps its dimensions while its content is hidden during the drag.
App.svelte
<script>
import { DragDropProvider, DragOverlay } from '@dnd-kit/svelte';
import Card from './Card.svelte';
</script>
<DragDropProvider>
<main style="padding: 80px; font-family: sans-serif;">
<p>Drag the card away, release, and watch the end of its return animation.</p>
<Card />
</main>
<DragOverlay>
<div style="box-sizing: border-box; width: 200px; padding: 24px; border: 2px solid #2563eb; background: #dbeafe; font-family: sans-serif;">Drag me</div>
</DragOverlay>
</DragDropProvider>Card.svelte
<script>
import { createDraggable } from '@dnd-kit/svelte';
const draggable = createDraggable({ id: 'card' });
</script>
<div style="width: 200px; user-select: none; touch-action: none;" {@attach draggable.attach}>
<div
data-card
style="box-sizing: border-box; width: 200px; padding: 24px; border: 2px solid #2563eb; background: #dbeafe;"
style:visibility={draggable.isDragSource ? 'hidden' : 'visible'}
>Drag me</div>
</div>- Open the reproduction in Chromium.
- Set CPU throttling to 4× slowdown to make the handoff easier to inspect.
- Drag the card several hundred pixels away from its original position.
- Release it and watch the end of the return animation.
The problem also reproduced without CPU throttling.
Expected behavior
The overlay remains visible until the source is visible again. No frame during the handoff should have both hidden.
Actual behavior
The overlay animates back correctly, disappears, and then the source becomes visible on a later frame.
A requestAnimationFrame observer sampled both elements' visibility until cleanup completed. The same source DOM node remained connected in every sample:
| CPU throttling | Existing timer cleanup: blank frame samples | Microtask cleanup: blank frame samples |
|---|---|---|
| None | 1 | 0 |
| 4× slowdown | 1 | 0 |
These are observations from the isolated reproduction, not a claim that every device will always show exactly one blank frame. A DOM assertion that only checks the final settled state misses this intermediate state.
Cause
In Feedback.ts, cleanup hides the overlay by removing its dragging attribute and resetting its styles. For an overlay, finalize then runs in a separate timer task.
finalize sets the source status to idle, allowing the drag operation to reset and Svelte to reveal the source. The task boundary permits rendering between hiding the overlay and revealing the source.
Proposed fix
Keep finalization asynchronous, but schedule it in a microtask:
if (feedbackElement === this.overlay) {
- setTimeout(finalize, 0);
+ queueMicrotask(finalize);
} else {
finalize();
}In the Svelte reproduction this lets the operation reset and the resulting DOM update finish before the next paint. The change is in the shared DOM feedback plugin; it does not require an application-level animation workaround.
Validation
- The standalone reproduction has no Query dependency or application data layer. The existing timer produced a blank frame at both 1× and 4×; replacing only that scheduling call removed it.
- Additional browser checks in a calendar application passed for planner → todo, todo → planner, and day → day at 4× slowdown.
- Reduced-motion drops, keyboard cancellation with focus restoration, a second drag after settling, and rollback after a rejected optimistic update also passed.
The patch has been validated with the Svelte adapter in Chromium. Other adapters and browsers have not been validated here. A regression test should inspect visibility across animation frames through the final overlay-to-source handoff, rather than only checking the final DOM.
Source: clauderic/dnd-kit