[BUG] Reorder.Item with dragConstraints gets a faulty drag offset on window resize
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.
Source: motiondivision/motion