[BUG] Window scroll is compensated twice during drag, making Reorder auto-scroll run away
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- cypress, react, typescript
Research direction
Start with PanSession.handlePointerMove and handleScroll, then read the existing window-scroll drag case in drag-scroll-while-drag.ts. Reproduce the reported event ordering: dispatch a pointermove whose pageY includes the new window scroll before its scroll event fires. Add a regression assertion that the dragged element does not move by the extra scroll delta, and confirm auto-scroll stays within maxSpeed for a pointer inside the viewport.
Written by the indexing model from the issue text.
Description
1. Read the FAQs 👇
Done.
2. Describe the bug
While dragging, PanSession.handleScroll adds the window scroll delta to lastMoveEventInfo.point, assuming the last pointer point was captured before that scroll. But if a pointermove is handled after the window has scrolled and before the matching scroll event fires, its pageY already includes the new scroll. handleScroll then adds the same delta a second time, so the drag point ends up below the real pointer by one frame's scroll amount.
With Reorder on a page that scrolls the document (no overflow: auto container, supported since #3508), this feeds back into itself:
- Auto-scroll reads the drifted point, decides the pointer is past the edge, and scrolls more than
maxSpeed(25px). The intensity is not capped at 1 when the pointer looks like it is beyond the edge. - The next
pointermovere-captures the real point, and the following scroll event adds that larger delta again. - The drift grows every round. In one Safari drag it went 95 → 156 → 347 → 1,426px over four pointermoves, and the next call scrolled 21,231px.
In practice the list jumps straight to the top or bottom while you drag. Sometimes the dragged item flies off far enough that the document gets taller.
Instrumented log on 12.43.0 (Chromium), with the pointer held still 2px above the bottom edge of an 800px viewport:
pointermove clientY 798, pageY 858 (= 798 + scrollY 60) ← correct
handleScroll delta 15 → point 858 + 15 = 873 ← counted twice, 15px too low
autoScroll viewport point 813 (real 798) → amount 39.7 ← above maxSpeed 25
handleScroll delta 39.5 → drift stays 15 …
The relevant code is unchanged on main (v14.3.0), and the repro below fails on 14.3.0.
3. CodeSandbox reproduction
https://codesandbox.io/s/4lw8kg (preview in a full window: https://4lw8kg.csb.app/)
It's a page-scrolling Reorder.Group with 40 items. It wraps window.scrollBy and counts auto-scroll calls above 25px while the pointer is inside the viewport, which the auto-scroll formula should never produce.
4. Steps to reproduce
- Open the sandbox in a full window.
- Drag "Item 3" down to the bottom edge of the window (stay inside the window).
- Hold still for a second, then wiggle slightly while staying at the edge.
- The readout shows calls above 25px. On wiggle the page jumps to the end, and single
scrollBycalls can reach thousands of px.
5. Expected behavior
The drag point should match the pointer, so a single auto-scroll call stays at or below 25px while the pointer is inside the viewport.
6. Video or screenshots
Measured with the repro on framer-motion 14.3.0 (Playwright, 1280×800, pointer 2px above the bottom edge):
| Hold still | Wiggle at edge | |
|---|---|---|
| Chromium | 38/47 calls > 25px, max 28.1px | max 14,151,139px; document grew from 5,668 to 43,372px |
| WebKit | 37/45 calls > 25px, max 27.0px | max 7,293px, jumped to the end |
On 12.43.0 with a real mouse or touch, the largest single call was 39.7px in Firefox 155, 1,714,438px in Safari 26.6.2 (macOS), and 17,073px with touch on iOS 18.7 (WKWebView). Chrome 155 with a real mouse showed up to 14.5px of drift.
7. Environment details
- framer-motion 14.3.0 and 12.43.0, React 19
- macOS: Chrome 155, Firefox 155, Safari 26.6.2; iOS 18.7 (WKWebView, touch)
Suggested fix
pageX/pageY from a pointer event already include the current window scroll, so reset the window baseline whenever a new pointer point is taken:
private handlePointerMove = (event: PointerEvent, info: EventInfo) => {
+ if (this.scrollPositions.has(window)) {
+ this.scrollPositions.set(window, { x: window.scrollX, y: window.scrollY })
+ }
this.lastMoveEvent = event
Element scroll is unaffected: element scroll doesn't change pageX/pageY, and that branch adjusts the history origin instead.
Results with this change applied to 14.3.0 in the same repro:
| Hold still | Wiggle at edge | |
|---|---|---|
| Chromium | 0/46 calls > 25px, max 23.0px | 0/46, max 23.0px |
| WebKit | 0/45 calls > 25px, max 23.0px | 0/40, max 23.0px |
23.0px is the expected 25 × (1 − 2/50)² for a pointer 2px from the edge, so the drift is gone. With the pointer held completely still, handleScroll still keeps the dragged item under the pointer while the page auto-scrolls. I checked this on 12.43.0: the offset stayed constant while scrollY went from 98 to 640.
A Cypress case could go next to "Drag with window scroll during drag" in drag-scroll-while-drag.ts: scroll the window, then dispatch a pointermove whose pageY already includes that scroll before the scroll event fires, and assert that the element doesn't move by the extra delta. I haven't written it.
Separately, getScrollAmount doesn't cap intensity at 1, so a pointer that really is beyond the edge (for example, a mouse dragged below the window) also scrolls faster than maxSpeed. Capping it would also bound the damage from any future drift.
- Dominant language
- TypeScript
- Stars
- 34.4k
- Forks
- 1.4k
- Avg merge
- 23h 37m
- Merged PRs (30d)
- 96
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from motiondivision/motion
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
motiondivision/motion#3935 · 1 comment ·
Maintainers usually reply within 1 day
-
[BUG] animate change made while inside a hidden <Activity> is ignored on revealPossibly taken @mattgperry claimed this today. Open
Difficulty 3/5 1-2 days Newbie friendliness 45/100
motiondivision/motion#3942 ·
Maintainers usually reply within 1 day
-
[FEATURE] AnimateSuspense for transitions between suspense fallbacks and streamed contentMay be free again A pull request for this issue was closed without being merged. Openfeature
Difficulty 5/5 Over a week Newbie friendliness 35/100
motiondivision/motion#3173 · 9 reactions ·
Maintainers usually reply within 1 day
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 35/100
motiondivision/motion#2662 · 2 comments · 4 reactions ·
Maintainers usually reply within 1 day
-
feature
Difficulty 4/5 3-5 days Newbie friendliness 35/100
motiondivision/motion#2658 · 1 reaction ·
Maintainers usually reply within 1 day
All issues in motiondivision/motion
Similar issues
-
[Android] Nested transforms are composed in reverse orderPossibly taken @Danyalo claimed this today. OpenRepro provided
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
software-mansion/react-native-svg#3090 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
radix-ui/primitives#4228 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 95/100
rocketride-org/rocketride-server#2705 · 1 comment ·
Maintainers usually reply within 1 day
-
archon-ready archon-size-s archon-type-bug area: web
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
coleam00/Archon#4707 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
HarperFast/grafana-datasource#177 ·