# List Drag Reorder — React Table Component

> A ranked list you reorder by dragging a grip handle (or Arrow keys); the lifted row floats with a shadow while the others spring out of its way.

- Category: [Tables & Lists](https://pixelpapa.com/components/tables.md)
- Tags: list, drag, reorder, sortable, spring
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/tables/list-drag-reorder

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/list-drag-reorder.json"
```

The file is written to `components/pixelpapa/list-drag-reorder.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { GripVertical } from "lucide-react";
import { Reorder, useDragControls, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type ReorderListProps<T> = {
  items: T[];
  onReorder: (items: T[]) => void;
  itemKey: (item: T) => string;
  itemLabel: (item: T) => string;
  renderItem: (item: T, index: number) => ReactNode;
  className?: string;
};

function Row<T>({
  item,
  index,
  count,
  label,
  onMove,
  children,
}: {
  item: T;
  index: number;
  count: number;
  label: string;
  onMove: (dir: -1 | 1) => void;
  children: ReactNode;
}) {
  const controls = useDragControls();
  const reduce = useReducedMotion();
  const [dragging, setDragging] = useState(false);
  const handle = useRef<HTMLButtonElement>(null);
  const refocus = useRef(false);

  // React may re-insert the moved node, which drops focus; put it back.
  useEffect(() => {
    if (refocus.current) handle.current?.focus();
    refocus.current = false;
  }, [index]);

  function onKeyDown(e: KeyboardEvent<HTMLButtonElement>) {
    if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
    e.preventDefault();
    refocus.current = true;
    onMove(e.key === "ArrowUp" ? -1 : 1);
  }

  return (
    <Reorder.Item
      value={item}
      dragListener={false}
      dragControls={controls}
      onDragStart={() => setDragging(true)}
      onDragEnd={() => setDragging(false)}
      whileDrag={reduce ? undefined : { scale: 1.025, rotate: -0.6 }}
      transition={{ type: "spring", stiffness: 480, damping: 36 }}
      className={cn(
        "relative flex items-center gap-2 rounded-xl border bg-card py-2.5 pr-4 pl-1.5 select-none",
        dragging ? "z-10 border-primary/40 shadow-xl shadow-primary/10" : "border-border shadow-xs",
      )}
    >
      <button
        ref={handle}
        type="button"
        aria-label={`Reorder ${label}. Position ${index + 1} of ${count}. Use arrow keys to move.`}
        onPointerDown={(e) => controls.start(e)}
        onKeyDown={onKeyDown}
        className={cn(
          "flex h-9 w-6 shrink-0 touch-none items-center justify-center rounded-md text-muted-foreground/60 outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60",
          dragging ? "cursor-grabbing text-primary" : "cursor-grab",
        )}
      >
        <GripVertical className="size-4" />
      </button>
      {children}
    </Reorder.Item>
  );
}

export function ReorderList<T>({ items, onReorder, itemKey, itemLabel, renderItem, className }: ReorderListProps<T>) {
  const [announce, setAnnounce] = useState("");

  function move(index: number, dir: -1 | 1) {
    const to = index + dir;
    if (to < 0 || to >= items.length) return;
    const next = [...items];
    [next[index], next[to]] = [next[to], next[index]];
    onReorder(next);
    setAnnounce(`${itemLabel(items[index])} moved to position ${to + 1} of ${items.length}`);
  }

  return (
    <>
      <Reorder.Group axis="y" values={items} onReorder={onReorder} className={cn("flex flex-col gap-2", className)}>
        {items.map((item, i) => (
          <Row key={itemKey(item)} item={item} index={i} count={items.length} label={itemLabel(item)} onMove={(d) => move(i, d)}>
            {renderItem(item, i)}
          </Row>
        ))}
      </Reorder.Group>
      <p aria-live="assertive" className="sr-only">
        {announce}
      </p>
    </>
  );
}

type Priority = { id: string; title: string; team: string; effort: string; owner: string; hue: string };

const seed: Priority[] = [
  { id: "p1", title: "Self-serve SSO setup", team: "Identity", effort: "3 wks", owner: "IV", hue: "bg-chart-1" },
  { id: "p2", title: "Usage-based billing alerts", team: "Billing", effort: "2 wks", owner: "TR", hue: "bg-chart-4" },
  { id: "p3", title: "Offline mode for the mobile app", team: "Mobile", effort: "6 wks", owner: "MO", hue: "bg-chart-3" },
  { id: "p4", title: "Audit log retention controls", team: "Platform", effort: "1 wk", owner: "JO", hue: "bg-chart-5" },
  { id: "p5", title: "Importer for legacy CSV exports", team: "Growth", effort: "2 wks", owner: "SB", hue: "bg-chart-2" },
];

export default function ListDragReorderDemo() {
  const [items, setItems] = useState(seed);

  return (
    <div className="w-full max-w-md">
      <div className="mb-3 flex items-baseline justify-between px-1">
        <h3 className="text-sm font-semibold text-foreground">Q1 priorities</h3>
        <p className="text-xs text-muted-foreground">Drag to rank · top three ship first</p>
      </div>
      <ReorderList
        items={items}
        onReorder={setItems}
        itemKey={(p) => p.id}
        itemLabel={(p) => p.title}
        renderItem={(p, i) => (
          <>
            <span
              className={cn(
                "flex size-6 shrink-0 items-center justify-center rounded-md text-xs font-semibold tabular-nums transition-colors duration-300",
                i < 3 ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground",
              )}
            >
              {i + 1}
            </span>
            <div className="min-w-0 flex-1 pl-1">
              <p className="truncate text-sm font-medium text-card-foreground">{p.title}</p>
              <p className="text-xs text-muted-foreground">
                {p.team} · {p.effort}
              </p>
            </div>
            <span className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white", p.hue)}>
              {p.owner}
            </span>
          </>
        )}
      />
    </div>
  );
}
```

## AI prompt

```text
Add the "List Drag Reorder" component from PixelPapa UI to this project.

What it is: A ranked list you reorder by dragging a grip handle (or Arrow keys); the lifted row floats with a shadow while the others spring out of its way.
Category: Tables & Lists. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/list-drag-reorder.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/list-drag-reorder.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/list-drag-reorder" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More tables & lists

- [Table Expandable Rows](https://pixelpapa.com/components/tables/table-expandable-rows.md) (Pro): Table rows that open a detail panel underneath with a spring height animation and a rotating chevron, one at a time or many.
- [Table Inline Status Edit](https://pixelpapa.com/components/tables/table-inline-status-edit.md): Each row carries a status pill that opens a keyboard-friendly dropdown in place; the pill swaps with a spring and the row confirms the save.
- [Table Row Select Bulk](https://pixelpapa.com/components/tables/table-row-select-bulk.md) (Pro): Checkbox rows with shift-click range selection and a floating action bar that springs up from the bottom with a rolling count.
- [Table Sortable Animated](https://pixelpapa.com/components/tables/table-sortable-animated.md): A semantic data table whose rows glide to their new positions with spring layout animation when a column header is clicked.
