Components

Drag-to-Reorder List

Grab a card and the others glide out of the way live. The dragged card follows the pointer with a raw translate (easing there reads as lag), each displaced sibling shifts exactly one slot on a retargetable transition, and the drop plays a FLIP: measure, reorder, invert, glide. Drag past the ends and it turns to rubber, friction instead of a wall. Fully keyboard operable too: focus a grip, Space grabs, ↑↓ move with the same glide, Escape puts it back. Tick Hard snap to feel the same math with zero interpolation.

  • Design reviewfigma
  • Ship mention popoverlab
  • Deploy stagingvercel
  • Write changelognotion
  • Announce releasesocial

Last move: none · drag a card or click a grip

Install

npx moumenlab add drag-to-reorder-list

For AI

Open .md

Usage

"use client";

import { useState } from "react";
import DragReorderList, { type ReorderItem } from "./drag-to-reorder-list";

const TASKS: ReorderItem[] = [
  { id: "design", label: "Design review", meta: "figma" },
  { id: "mention", label: "Ship mention popover", meta: "lab" },
  { id: "staging", label: "Deploy staging", meta: "vercel" },
  { id: "changelog", label: "Write changelog", meta: "notion" },
  { id: "announce", label: "Announce release", meta: "social" },
];

export default function DragReorderListExample() {
  // Controlled: hold the order yourself and persist it in onReorder.
  const [items, setItems] = useState(TASKS);
  return (
    <div className="flex flex-col items-center gap-6">
      <DragReorderList items={items} onReorder={setItems} />

      {/* flip={false} is the hard-snap comparison - same math, no glides. */}
      <DragReorderList defaultItems={TASKS.slice(0, 3)} flip={false} />
    </div>
  );
}

Story

  1. The card has to feel held

    Any easing between your hand and the card reads as lag, so the dragged card's y is pushed straight from the pointer with nothing in between and no React render on the way. The rows it displaces are the opposite: each one glides exactly one slot, and reversing mid-glide just retargets.

  2. The drop is where it usually breaks

    Committing the new order re-renders the list, and the card jumps to wherever the DOM put it. So the drop is a FLIP: measure every rect first, let the reflow happen, put each card back at its old screen pixels, then animate it home.

  3. It works fully from the keyboard

    A reorder that only exists under a pointer is half a component. The grip is a real focusable button: Space grabs, arrows move, Escape cancels, Space drops. Every move is announced through a live region, so the new position is stated rather than only shown.

References

  • setPointerCapture (MDN): what keeps the drag attached once the pointer leaves the row: events stay targeted at the card until release, so a fast drag never gets dropped mid-air.
  • Easing Graphs: where the one curve in here got picked. The glide and the FLIP drop both run on cubic-bezier(0.22, 1, 0.36, 1), and its graph is nearly flat by the halfway mark, so a row covers most of its slot immediately and then settles. Compare Expo Out and Swift Out against the softer Quad and Cubic curves to see why the gentler ones read as sliding instead.