Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-dragoverlay-flicker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dnd-kit/dom": patch
---

Fix DragOverlay flickering after drop
52 changes: 29 additions & 23 deletions packages/dom/src/core/plugins/feedback/Feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ export class Feedback extends Plugin<DragDropManager, FeedbackOptions> {
constructor(manager: DragDropManager, options?: FeedbackOptions) {
super(manager, options);

const styleInjector = manager.registry.plugins.get(
StyleInjector as any
) as StyleInjector | undefined;
const styleInjector = manager.registry.plugins.get(StyleInjector as any) as
| StyleInjector
| undefined;

const unregisterStyles = styleInjector?.register(CSS_RULES);

Expand Down Expand Up @@ -461,29 +461,37 @@ export class Feedback extends Plugin<DragDropManager, FeedbackOptions> {
feedbackElement.removeAttribute(ATTRIBUTE);
styles.reset();

if (savedCellWidths && isTableRow(element)) {
const cells = Array.from(element.cells);
const finalize = () => {
if (savedCellWidths && isTableRow(element)) {
const cells = Array.from(element.cells);

for (const [index, cell] of cells.entries()) {
cell.style.width = savedCellWidths[index] ?? '';
for (const [index, cell] of cells.entries()) {
cell.style.width = savedCellWidths[index] ?? '';
}
}
}

source.status = 'idle';
source.status = 'idle';

const moved = state.current.translate != null;
const isDragging = dragOperation.status.dragging;
const moved = state.current.translate != null;
const isDragging = dragOperation.status.dragging;

if (
placeholder &&
((!isDragging && moved) ||
placeholder.parentElement !== feedbackElement.parentElement) &&
feedbackElement.isConnected
) {
placeholder.replaceWith(feedbackElement);
}
if (
placeholder &&
((!isDragging && moved) ||
placeholder.parentElement !== feedbackElement.parentElement) &&
feedbackElement.isConnected
) {
placeholder.replaceWith(feedbackElement);
}

placeholder?.remove();
placeholder?.remove();
};

if (feedbackElement === this.overlay) {
setTimeout(finalize, 0);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like await manager.renderer.rendering have worked here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I'll look into it and update the PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

} else {
finalize();
}
};

/* ---- Reactive effects ---- */
Expand Down Expand Up @@ -511,9 +519,7 @@ export class Feedback extends Plugin<DragDropManager, FeedbackOptions> {
const currentShape = untracked(() => dragOperation.shape?.current);
const keyboardTransition = options?.keyboardTransition;
const translateTransition =
isKeyboardOperation &&
!reducedMotion &&
keyboardTransition !== null
isKeyboardOperation && !reducedMotion && keyboardTransition !== null
? `${keyboardTransition?.duration ?? 250}ms ${keyboardTransition?.easing ?? 'cubic-bezier(0.25, 1, 0.5, 1)'}`
: '0ms linear';

Expand Down
Loading