All components

Sortable List

Data Display

Drag-to-reorder, hand-written, with a keyboard mode that shares the same code path. The target index is a pure function of geometry frozen at lift and the pointer delta — monotonic in the delta by construction, so there is no feedback loop to oscillate in at any drag speed. Pointer capture, edge auto-scroll, a drop animation into the final slot, and `aria-live` announcements throughout.

Drag, or focus a row and press space

  • Pointer capture
    drag survives leaving the row
  • Cached geometry
    snapshotted once at lift — the whole reason this doesn't oscillate
  • Auto-scroll
    near the container edges
  • Keyboard mode
    space, arrows, space
  • Live announcements
    aria-live on every move
  • Drop animation
    glides into the final slot
  • Ragged heights
    layout is exact, not a uniform pitch
  • Zero re-renders
    transforms written straight to the DOM

Try to make it oscillate: park the cursor exactly on a boundary and jiggle. The target index is a pure function of the frozen snapshot and the pointer delta, so there is no feedback loop to buzz in.

Tab to a row, then Space to lift, ↑↓ to move, Space to drop, Esc to cancel — the entire flow without a mouse.

a → b → c → d → e → f → g → h

Installation

Terminal
npx shadcn@latest add https://ui.saumyarex.xyz/r/sortable-list.json

Props

PropTypeDefaultDescription
itemsT[]The list, fully controlled. Rows may have different heights — the displacement layout is computed exactly, not from an assumed uniform pitch.
getId(item: T) => string | numberStable React key, and the fallback announcement label.
onReorder(next: T[]) => voidCalled once, on drop, with the reordered array — never mid-drag.
getLabel(item: T) => stringHuman name used in the `aria-live` announcements. Defaults to the id.
children(item: T, state: { dragging, index }) => React.ReactNodeRow content. The component owns the `<li>`, its focus handling, and its transforms.
handlebooleanfalseOnly start a pointer drag from an element marked `data-sortable-handle` inside the row. Keyboard is unaffected — the row itself is always focusable and operable, so a handle can never lock keyboard users out.
autoScrollbooleantrueScroll the nearest scrollable ancestor when dragging near its edges. The geometry cache is in content coordinates, so scrolling corrects one pointer-delta term rather than invalidating every cached row.

Dependencies

clsxtailwind-merge