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.
- 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
<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.
<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.
- Dragged
- follows the pointer, lift via data-dragging
- Neighbor
- springs into the vacated slot — same REORDER_SPRING
// 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 };<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.
<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
- 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.