fix(feedback): prevent DragOverlay flickering after drop#2020
fix(feedback): prevent DragOverlay flickering after drop#2020clauderic merged 1 commit intoclauderic:mainfrom
Conversation
🦋 Changeset detectedLatest commit: 00fd955 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Can you try solving it at this layer instead of applying inline styles? The library intentionally avoids applying any inline styles as much as possible. |
|
Thanks for the review! Agreed, inline styles aren't the right layer. I'll rework the fix at the stylesheet layer and push an update. |
|
Thank you for the feedback! I've updated the fix to avoid inline styles. Instead of setting This keeps the injected CSS rule |
| }; | ||
|
|
||
| if (feedbackElement === this.overlay) { | ||
| setTimeout(finalize, 0); |
There was a problem hiding this comment.
Would something like await manager.renderer.rendering have worked here?
There was a problem hiding this comment.
Thank you! I'll look into it and update the PR
There was a problem hiding this comment.
Thanks for the suggestion! I looked into whether manager.renderer.rendering
could work here, but I believe it wouldn't reliably fix the flicker.
At the point where cleanup() is called (inside animateTransform().then()),
there is no React rendering in progress — rendering.current is null, so
manager.renderer.rendering returns Promise.resolve(). This means the
.then(finalize) callback would run as a microtask, which executes before
the browser paint — the same timing issue as calling finalize() directly.
The flicker occurs because the injected CSS rules are removed (synchronously,
via StyleInjector) before the browser has a chance to paint with them applied.
To fix this, finalize() — which triggers source.status = 'idle' and
ultimately the CSS rule removal — needs to be deferred until after paint.
In the browser event loop: Task → Microtask → rAF → Paint → next Task (setTimeout)
setTimeout(fn, 0) defers execution to after paint (unlike microtasks or
requestAnimationFrame, which both run before paint). I don't see an existing abstraction in the codebase for post-paint
deferral, so I think setTimeout is the right tool here.
That said, happy to explore other approaches if you have something specific in mind!
@dnd-kit/abstract
@dnd-kit/collision
@dnd-kit/dom
@dnd-kit/geometry
@dnd-kit/helpers
@dnd-kit/react
@dnd-kit/solid
@dnd-kit/state
@dnd-kit/svelte
@dnd-kit/vue
commit: |
Fixes #1996
Problem
When using
DragOverlay, a brief flicker occurs after dropping an item.The overlay momentarily appears unstyled (at its natural/full size) before disappearing.
Root Cause
The flicker is caused by a timing gap between style removal and React's re-render:
cleanup()callsstyles.reset()— removes all CSS custom properties from the overlay elementsource.status = 'idle'is set immediately afterdragOperation.reset()→status = IdleStyleInjectorreacts synchronously and removes the injected CSS rules from the document(including
[data-dnd-overlay]:not([data-dnd-dragging]) { display: none })but React hasn't re-rendered yet to remove the children
The existing CSS rule
[data-dnd-overlay]:not([data-dnd-dragging]) { display: none }wasintended to handle this, but it gets removed from the document in the same synchronous
execution before the browser has a chance to paint with it applied.
Fix
Defer
source.status = 'idle'(and subsequent finalization) to after the browser paintby wrapping it in
setTimeout(fn, 0)when operating in overlay mode.This keeps the injected CSS rules — including
[data-dnd-overlay]:not([data-dnd-dragging]) { display: none }— alive in the documentat paint time, allowing the browser to apply them and hide the overlay before React
re-renders to remove the children.
setTimeoutis the only deferral mechanism that executes after the browser paint;microtasks and
requestAnimationFrameboth fire before paint.Only applied when
feedbackElement === this.overlayto avoid affecting non-overlay drag behavior.No inline styles are used.
Changes
packages/dom/src/core/plugins/feedback/Feedback.tscleanup(): extract finalization logic into afinalize()function; call it viasetTimeout(finalize, 0)when in overlay mode, or call it directly otherwiseVerification
cd apps/stories && bun run devThis is my first contribution to dnd-kit. Happy to make any changes if needed.