MynduiMyndui
0

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.

01/03Group, 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.Itemvalue={value}data-dragging={dragging || undefined}transition={REORDER_SPRING}onDragStart={() => setDragging(true)}onDragEnd={() => setDragging(false)}className="... [transition:scale_150ms_ease] data-[dragging]:scale-[1.03]">
01

Group, item, and a data-driven lift

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.

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.

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"
/>
02/03Neighbor flow
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 };
02

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.

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.

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>
03/03Result
  • Draft proposal
  • Review with design
  • Ship to staging
  • Announce
03

The result

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.

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.

Motion Score

Reorder ListSSCompositor-only
SscaleScale spring / press
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →