GodUIGodUI
81

Anatomy of Reorder List

How a data attribute (not whileDrag) drives the grab lift, and how one spring resolves the dragged item and every displaced neighbor together.

Reorder List leans almost entirely on Motion's built-in drag-to-reorder primitives — Reorder.Group and Reorder.Item already know how to swap values and animate the rest of the list around a drag. The interesting decisions here are in what this component deliberately does not let Framer drive: the lift.

Group, item, and a data-driven lift

Each row is a Reorder.Item with a value inside one Reorder.Group holding the ordered array and the onReorder callback. The grab lift — scale-[1.03] and a shadow — isn't a Framer whileDrag animation. It's a plain data-dragging attribute flipped by onDragStart/onDragEnd, read by a Tailwind data-[dragging]:scale-[1.03] and a CSS transition.

Anatomygroup, item, and a data-driven lift
Group
Reorder.Group, holds values + axis
Item
Reorder.Item, one value each
Lift
data-dragging → scale 1.03, CSS transition
Shadow
sibling overlay, opacity fade only
tsx
 
<Reorder.Item
  value={value}
  data-dragging={dragging || undefined}
  transition={REORDER_SPRING}
  onDragStart={() => setDragging(true)}
  onDragEnd={() => setDragging(false)}
  className="... [transition:scale_150ms_ease] data-[dragging]:scale-[1.03]"
>

whileDrag was the first approach, and it has a real failure mode: a scale/boxShadow animation driven that way can get stranded as an inline style if a drop lands mid-gesture, leaving the row visibly wider or shadowed afterward. A CSS class keyed off a data attribute always resets cleanly — when dragging flips back to false, the CSS transition just runs in reverse.

The lift shadow lives on a sibling overlay whose opacity fades in, rather than transitioning box-shadow on the row itself — box-shadow is a main-thread paint property; opacity is compositor-only.

tsx
 
<span
  aria-hidden
  className="pointer-events-none absolute inset-0 rounded-lg opacity-0 shadow-xl [transition:opacity_200ms_ease] group-data-[dragging]:opacity-100"
/>

Neighbor flow

What Framer does own is the reordering itself. The moment a dragged item's center crosses a neighbour's, Reorder.Group swaps them in the values array and calls onReorder — every item, dragged or not, then animates to its new slot under the same REORDER_SPRING.

Neighbor flowstiffness 520 · damping 32
Dragged
follows the pointer, lift via data-dragging
Neighbor
springs into the vacated slot — same REORDER_SPRING
tsx
 
// Snappy enough that neighbours flow out of the way instantly, but with a
// little spring so items settle rather than snap.
const REORDER_SPRING = { type: "spring" as const, stiffness: 520, damping: 32 };
tsx
 
<Reorder.Item value={value} transition={REORDER_SPRING}>

Stiffness 520 with damping 32 is snappier than the 320/32 spring used elsewhere in the library — a list reorder needs to feel instantaneous, not smoothed, or the displaced rows read as sluggish while a hand is still holding the dragged one.

Why the position lives in values, not local state

ReorderList never keeps its own copy of the order. values and onReorder are both owned by the caller — the component is a rendering and gesture layer over state you already control, so anything downstream of the list (persistence, undo, syncing to a server) just reacts to the same setState call the drag already triggers.

tsx
 
<ReorderList values={items} onReorder={setItems}>
  {items.map((item) => (
    <ReorderItem key={item.id} value={item}>{item.label}</ReorderItem>
  ))}
</ReorderList>

Accessibility and reduced motion

Rows are real <li> elements inside a <ul> (Reorder.Group's as="ul" and Reorder.Item's as="li"), so list semantics survive for anyone using a screen reader. axis can flip to "x" for a horizontal list without changing anything else. Under prefers-reduced-motion, both the scale transition and the shadow's opacity transition drop out (motion-reduce:transition-none) — the lift is skipped, though the drag gesture itself still works.

The result

Resultthe real component — drag a row by its handle
  • Draft proposal
  • Review with design
  • Ship to staging
  • Announce

One built-in reorder primitive for the list, one intentionally CSS-driven lift so a stray drop can never strand an inline style — that's the whole mechanism.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub