Skip to content

fix(feedback): prevent DragOverlay flickering after drop#2020

Merged
clauderic merged 1 commit intoclauderic:mainfrom
namgi2386:main
Apr 26, 2026
Merged

fix(feedback): prevent DragOverlay flickering after drop#2020
clauderic merged 1 commit intoclauderic:mainfrom
namgi2386:main

Conversation

@namgi2386
Copy link
Copy Markdown
Contributor

@namgi2386 namgi2386 commented Apr 17, 2026

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:

  1. cleanup() calls styles.reset() — removes all CSS custom properties from the overlay element
  2. source.status = 'idle' is set immediately after
  3. This synchronously triggers dragOperation.reset()status = Idle
  4. StyleInjector reacts synchronously and removes the injected CSS rules from the document
    (including [data-dnd-overlay]:not([data-dnd-dragging]) { display: none })
  5. The browser paints at this point — the overlay has no inline styles and no CSS rules,
    but React hasn't re-rendered yet to remove the children
  6. React re-renders on the next task and removes the children

The existing CSS rule [data-dnd-overlay]:not([data-dnd-dragging]) { display: none } was
intended 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 paint
by 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 document
at paint time, allowing the browser to apply them and hide the overlay before React
re-renders to remove the children.

setTimeout is the only deferral mechanism that executes after the browser paint;
microtasks and requestAnimationFrame both fire before paint.

Only applied when feedbackElement === this.overlay to avoid affecting non-overlay drag behavior.
No inline styles are used.

Changes

packages/dom/src/core/plugins/feedback/Feedback.ts

  • In cleanup(): extract finalization logic into a finalize() function; call it via
    setTimeout(finalize, 0) when in overlay mode, or call it directly otherwise

Verification

  1. Run storybook: cd apps/stories && bun run dev
  2. Open: http://localhost:6006/?path=/story/react-draggable-drag-overlay--drag-overlay
  3. Drag and drop the item — the overlay no longer flickers on drop

This is my first contribution to dnd-kit. Happy to make any changes if needed.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 00fd955

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@dnd-kit/dom Patch
@dnd-kit/abstract Patch
@dnd-kit/collision Patch
@dnd-kit/geometry Patch
@dnd-kit/helpers Patch
@dnd-kit/react Patch
@dnd-kit/state Patch
@dnd-kit/vue Patch
@dnd-kit/solid Patch
@dnd-kit/svelte Patch

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

@clauderic
Copy link
Copy Markdown
Owner

The existing CSS rule [data-dnd-overlay]:not([data-dnd-dragging]) { display: none } was
intended 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.

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.

@namgi2386
Copy link
Copy Markdown
Contributor Author

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.

@namgi2386
Copy link
Copy Markdown
Contributor Author

Thank you for the feedback!

I've updated the fix to avoid inline styles. Instead of setting style.display = 'none', I now defer source.status = 'idle' (and the rest of the cleanup finalization) to after the browser paint using setTimeout(fn, 0).

This keeps the injected CSS rule [data-dnd-overlay]:not([data-dnd-dragging]) { display: none } alive in the document at paint time, so the browser can apply it to hide the overlay before React re-renders to remove the children.

};

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!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 25, 2026

Open in StackBlitz

@dnd-kit/abstract

npm i https://pkg.pr.new/@dnd-kit/abstract@2020

@dnd-kit/collision

npm i https://pkg.pr.new/@dnd-kit/collision@2020

@dnd-kit/dom

npm i https://pkg.pr.new/@dnd-kit/dom@2020

@dnd-kit/geometry

npm i https://pkg.pr.new/@dnd-kit/geometry@2020

@dnd-kit/helpers

npm i https://pkg.pr.new/@dnd-kit/helpers@2020

@dnd-kit/react

npm i https://pkg.pr.new/@dnd-kit/react@2020

@dnd-kit/solid

npm i https://pkg.pr.new/@dnd-kit/solid@2020

@dnd-kit/state

npm i https://pkg.pr.new/@dnd-kit/state@2020

@dnd-kit/svelte

npm i https://pkg.pr.new/@dnd-kit/svelte@2020

@dnd-kit/vue

npm i https://pkg.pr.new/@dnd-kit/vue@2020

commit: 00fd955

@clauderic clauderic merged commit 79ac91b into clauderic:main Apr 26, 2026
8 of 9 checks passed
@github-actions github-actions Bot mentioned this pull request Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flickering while using DragOverlay

2 participants