motiondivision / motiondivision/motion
[BUG] Reorder.Item with dragConstraints gets a faulty drag offset on window resize
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 33.7k
- Forks
- 1.4k
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 14
Description
Describe the bug
A Reorder.Item that has never been dragged gets a non-zero x (and z-index: 1) applied whenever the window resizes, as long as dragConstraints is a ref object. The items visually shift out of their layout positions and begin to overlap, so pointer hit-testing no longer matches what the user sees — clicking one item selects another. The first pointerdown on each item snaps that item back to its origin, so the problem clears itself one item at a time, one bad click each.
IMPORTANT: Provide a CodeSandbox reproduction of the bug
Apologies, I'm in a corporate network and can't access CodeSandbox (https://codesandbox.io/). But I do have a code snippet that can be pasted in there:
import React, { useRef, useState } from 'react';
import { Reorder } from 'framer-motion';
export function App() {
const containerRef = useRef(null);
const [items, setItems] = useState(['one', 'two', 'three', 'four', 'five']);
const [selected, setSelected] = useState('one');
return (
<Reorder.Group
ref={containerRef}
axis="x"
values={items}
onReorder={setItems}
style={{ display: 'flex', gap: 8, listStyle: 'none', padding: 8, margin: 0 }}
>
{items.map((item) => (
<Reorder.Item
key={item}
value={item}
dragConstraints={containerRef}
style={{ border: '1px solid #999', background: selected === item ? '#bcd' : '#eee' }}
>
<button type="button" style={{ padding: '10px 18px' }} onClick={() =>
setSelected(item)}>
{item}
</button>
</Reorder.Item>
))}
</Reorder.Group>
);
}
Steps to reproduce
- Load at a window width where the items fit comfortably (~1200px).
- Narrow the window until the row is tight (~500px). Don't drag anything.
- Every
<li>now has an inline transform: translateX(px) and z-index: 1. - Click an item — a different one gets selected. Click again — the right one is selected.
Expected behavior
An item that is not being dragged and is resting at its origin should keep x === 0 across a resize. z-index should stay unset.
Video or screenshots
N/A
Environment details
Windows, Chrome
Notes
Claude had a recommended root cause fix; here's its analysis:
VisualElementDragControls.scalePositionWithinConstraints() is wired to a window resize listener and to ResizeObservers on the element and the constraints element (addListeners()), and it never checks whether a drag is in progress:
eachAxis((axis) => {
if (!shouldDrag(axis, drag, null)) return;
const axisValue = this.getAxisMotionValue(axis);
const { min, max } = this.constraints[axis];
axisValue.set(mixNumber(min, max, boxProgress[axis]));
});
boxProgress is captured earlier as the element's position expressed as a fraction of the old constraints box. For an element resting at the origin, x === 0 is not a meaningful fraction of that box — so re-projecting it into the newly measured box yields a non-zero value. The function's stated purpose ("reposition the element within those new constraints relative to where it was before the resize") is correct for an element with a real offset, but for one at its origin there is no offset to preserve.
Confirmed by wrapping the item's x MotionValue and capturing the write:
value=480.998
at ReorderItemComponent.point.x.set
at eachAxis
at VisualElementDragControls.scalePositionWithinConstraints
at <window "resize" listener>
Suggested fix: In VisualElementDragControls.scalePositionWithinConstraints():
...
const axisValue = this.getAxisMotionValue(axis);
if (!this.isDragging && axisValue?.get() === 0) return;
const { min, max } = (this.constraints as ResolvedConstraints)[
axis
] as Axis
...
This is deliberately narrower than gating the whole function on isDragging, which would also stop a genuinely-offset draggable from being kept inside its constraints on resize. We've been running this as a build-time patch; it fixes the resize trigger, and drag-to-reorder is unaffected.
Impact: This affects any Reorder list with ref-based dragConstraints. One example in the wild: GraphiQL 5 renders each query tab as a Reorder.Item with dragConstraints={tabContainerRef}, so every GraphiQL user has to click each tab twice after any resize, zoom, or panel toggle.
Contributor 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.
Research direction
Start at VisualElementDragControls.scalePositionWithinConstraints() and its resize and ResizeObserver wiring in addListeners(); use the provided Reorder.Item reproduction with ref-based dragConstraints to observe the unwanted MotionValue update. Done means an idle item remains at x === 0 with no z-index after resizing, while an item with a real drag offset still stays within its constraints and drag-to-reorder remains unaffected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100