motiondivision / motiondivision/motion

[BUG] Reorder.Item with dragConstraints gets a faulty drag offset on window resize

Open
#3,823 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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

  1. Load at a window width where the items fit comfortably (~1200px).
  2. Narrow the window until the row is tight (~500px). Don't drag anything.
  3. Every <li> now has an inline transform: translateX(px) and z-index: 1.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.