[BUG] Reorder auto-scroll speed scales with the display's refresh rate
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- react, typescript
- Domain
- frontend, performance
Research direction
Start with getScrollAmount in packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts and trace how Reorder.Item calls autoScrollIfNeeded during dragging. Check how frameData.delta is exposed from motion-dom and how existing tests cover auto-scroll. Done means auto-scroll travels at a consistent speed across frame rates while retaining the expected speed at 60fps.
Written by the indexing model from the issue text.
Description
1. Read the FAQs 👇
Done.
2. Describe the bug
Reorder auto-scroll moves a fixed number of pixels per frame, so how fast the page scrolls depends on how many frames the browser runs per second. On a 120Hz display it scrolls twice as fast as on a 60Hz display (measured below), and a 240Hz display would be four times as fast.
getScrollAmount in packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts returns maxSpeed * intensity², with maxSpeed = 25. Reorder.Item calls autoScrollIfNeeded from onDrag, which runs every frame while the item is held near an edge, and each call scrolls by that amount. Nothing scales it by elapsed time (frameData.delta).
This is unchanged on main after #3951.
3. CodeSandbox reproduction
https://codesandbox.io/s/vxj4px (full window: https://vxj4px.csb.app/)
A page-scrolling Reorder.Group with 200 items on framer-motion 14.4.0. A panel shows Motion's frames per second and how far auto-scroll moved the page in the last second.
?fps=30 throttles requestAnimationFrame to 30fps, so the difference shows up without a high refresh rate display. It's installed before framer-motion is imported, because the frameloop captures requestAnimationFrame on load.
4. Steps to reproduce
- Open the full window preview.
- Drag "Item 3" to about 10px above the bottom edge and hold still.
- Note "scrolled in last 1s".
- Repeat with
?fps=30added to the URL, or on a display with a different refresh rate.
5. Expected behavior
The same scroll speed in pixels per second regardless of refresh rate, like Motion's time-based animations.
6. Video or screenshots
Measured with Playwright on 14.4.0, pointer held 10px above the bottom edge of an 800px viewport, over 2 seconds. Headed Chromium on a 120Hz ProMotion display; the 60fps and 30fps rows use ?fps=:
| Display frames | Auto-scroll calls | Page scrolled | Per call (avg) | |
|---|---|---|---|---|
| 120fps (native) | 240 | 315 | 4,530px | 14.4px |
?fps=60 |
120 | 158 | 2,268px | 14.4px |
?fps=30 |
60 | 71 | 976px | 13.7px |
Headless WebKit at its native 60fps vs ?fps=30: 2,095px vs 1,029px.
The amount per call stays the same, so the speed follows the frame rate: 120Hz scrolls twice as fast as 60Hz.
Calls outnumber display frames by 20–30% at every rate. That's the projection system's synchronous flush after React commits a layout change, here each reorder (create-projection-node.ts, "Manually flush any pending updates"), which runs the update step, and so PanSession.updatePoint → onDrag → autoScrollIfNeeded, a second time in the same display frame. That flush sets frameData.delta to the real elapsed time, a few ms, but the per-frame amount scrolls the full distance again. Scaling by frameData.delta handles that case as well.
Suggested fix
Treat maxSpeed as pixels per second and scale by the frame delta. 1500px/s keeps today's speed at 60Hz:
+import { frameData } from "motion-dom"
+
const threshold = 50
-const maxSpeed = 25
+// Pixels per second (25px per frame at 60fps)
+const maxSpeed = 1500
function scrollSpeed(distanceFromEdge: number): number {
const intensity = Math.min(1, 1 - distanceFromEdge / threshold)
- return maxSpeed * intensity * intensity
+ return maxSpeed * intensity * intensity * (frameData.delta / 1000)
}
frameData.delta is already clamped to 40ms, so a long frame scrolls at most 60px.
For reference, Atlassian's pragmatic-drag-and-drop made the same call in its auto-scroll package (configuration.ts): "Using 'per second' rather than 'per frame' as we want a consistent scroll speed regardless of frame rate", with its fast preset defined as 25 * 60 px/s.
With this applied to 14.4.0 in the same setup, the distance in 2 seconds no longer depends on the frame rate: 1,898px at 120fps, 1,872px at 60fps and 1,732px at 30fps (Chromium), 1,795px vs 1,733px at 60 and 30fps (WebKit).
7. Environment details
- framer-motion 14.4.0, React 19.2.0
- macOS 26.6.2, MacBook Pro (120Hz ProMotion)
- Playwright Chromium 156 (headed) and WebKit 27.2 (headless)
- Dominant language
- TypeScript
- Stars
- 34.4k
- Forks
- 1.4k
- Avg merge
- 23h 3m
- Merged PRs (30d)
- 108
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
-
[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] Keyboard accessible reorderingPossibly taken @mattgperry claimed this 1 day ago. Openfeature
Difficulty 4/5 3-5 days Newbie friendliness 35/100
motiondivision/motion#2658 · 1 reaction ·
Maintainers usually reply within 1 day
-
bug
Difficulty 3/5 1-2 days Newbie friendliness 35/100
motiondivision/motion#2546 · 4 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
[FEATURE] Manually update animations without `requestAnimationFrame`Possibly taken A pull request linked to this issue is open or already merged. Openfeature
Difficulty 5/5 Over a week Newbie friendliness 30/100
motiondivision/motion#2496 · 2 comments · 3 reactions ·
Maintainers usually reply within 1 day
All issues in motiondivision/motion
Similar issues
-
bug P3 UI/UX
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
StellarState/SS-backend#712 ·
Maintainers usually reply within 1 day
-
area: backend bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
snapotter-hq/SnapOtter#3473 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
gitroomhq/postiz-app#2304 ·
Maintainers usually reply within 1 day
-
harlan-agent-ready-to-implement routine:ci-review
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
harlan-zw/unlighthouse#454 · 2 comments ·
Maintainers usually reply within 1 day