Drag-to-reorder

How to drag-to-reorder with Motion

Work in progress

This article is still being written and may change.

These tag chips do two things at once: tap one to toggle it, or drag one to rearrange the row. The drag-and-drop is Motion's Reorder - about fifteen lines, with spring layout animations for free. Try it:

Drag to reorder · tap to toggle

  • css
  • react
  • animation
  • webgl
  • design

Two components, two props

Motion's Reorder is a Group wrapped around Items. You hand the group the array and an onReorder callback; it hands you back the reordered array when a drag settles. Each item declares which value it represents. That's the whole contract - drag, layout animation and reordering are handled for you.

import { Reorder } from "motion/react"

const [tags, setTags] = useState(["css", "react", "animation"])

<Reorder.Group axis="x" values={tags} onReorder={setTags}>
  {tags.map((tag) => (
    <Reorder.Item key={tag} value={tag}>
      {tag}
    </Reorder.Item>
  ))}
</Reorder.Group>

Group renders a <ul> and Item a <li> by default; both take an as prop. axis is "y" by default - a vertical list - so a horizontal row needs axis="x". The keys must be stable and the value must exist in the group's values array, or the swap logic has nothing to match against.

Letting a chip be both draggable and tappable

The chips already had a job before they could be dragged: clicking one toggles a filter. Drag and click now share the same element, so a click handler would fire at the end of every drag. The fix is one ref: clear it on pointerDown, set it the moment a real drag starts, and let the click through only when no drag happened.

const dragging = useRef(false)

<Reorder.Item
  value={tag}
  onPointerDown={() => (dragging.current = false)}
  onDragStart={() => (dragging.current = true)}
  onClick={() => { if (!dragging.current) toggle(tag) }}
  whileDrag={{ scale: 1.06 }}>
  {tag}
</Reorder.Item>

onDragStart only fires once Motion clears its small drag threshold, so a plain tap never trips it. The whileDrag scale is the bit of feedback that makes the chip feel picked up.

Keep it on one line

This is the trap that cost me the most time. The row started as a flex-wrap container, and reordering tore open a huge gap between chips. With axis="x", Motion compares each item's horizontal range to decide when to swap - but a wrapped second row stacks items whose x ranges overlap the first row's. The swap logic gets contradictory geometry and shoves a chip onto its own line.

A single-axis reorder needs a single axis. Drop flex-wrap and let the row scroll instead - now there are no rows to jump between, and the gap is gone.

// gaps appear - items wrap onto a second row
<Reorder.Group axis="x" className="flex flex-wrap gap-2" >

// clean - one line, horizontal scroll on overflow
<Reorder.Group axis="x" className="flex gap-2 overflow-x-auto" >

One more line of housekeeping the docs call out: a dragged item lifts above its neighbours only if it forms a stacking context, so give each Item position: relative. Motion sets a z-index during the drag, and z-index is ignored on position: static elements.

Don't lose the keyboard

Drag-to-reorder is pointer-only - there's no keyboard equivalent, and a screen reader can't grab a chip. That's acceptable here because the order is cosmetic: nothing depends on it. What can't regress is the meaningful action. Each chip stays a real control - role="button", aria-pressed, and an onKeyDown so Enter and Space toggle the filter. Reordering is a bonus on top of a fully operable button, not a replacement for it.

And, as always, the drag's layout animation should respect prefers-reduced-motion - wrap the group in <MotionConfig reducedMotion="user"> and the reflow becomes instant for anyone who's asked for less motion, while the drag itself still works.

When to reach for it

Reorder is the right tool when the order means something a user wants to control - a playlist, a kanban column, ranked preferences, a to-do list. It is the wrong tool for a control like these filters, where I added it for delight rather than function and had to spend the effort to keep tap-and-keyboard intact anyway. Worth it on a portfolio; I'd think twice in a product. Either way, the full API is small and well worth reading the Reorder docs for.

Newsletter

Stay updated with my latest articles and projects. No spam, no nonsense.