renderpx
Theme: auto

Drag-and-Drop

Reorderable lists with mouse, touch, and keyboard support. The browser's native drag API looks tempting but breaks on mobile and inside scroll containers. Use dnd-kit instead.

The problem I keep seeing

Drag-and-drop feels simple until you need it to actually work. Developers reach for the browser's native draggable attribute and onDragOver/onDrop events, which work for basic mouse drag on desktop. But they break on iOS entirely (Safari ignores the touch events), produce an ugly browser ghost image, give no drop indicator, and clip when the parent has overflow: hidden. Rolling your own from raw pointer events is worse.

The real requirement is usually: reorder items in a list, show a drag preview that doesn't clip, work on touch and with keyboard, sync the new order to the server, and roll back if the mutation fails.

Naive approach

Native HTML drag events. Works for mouse on desktop; fails everywhere else.

tsx
Loading...

First improvement

dnd-kit is the right library for React DnD in 2024+. It abstracts sensors (pointer, touch, keyboard) behind a composable API, handles ARIA announcements for accessibility, and gives you CSS transforms for smooth movement instead of browser ghost images. The useSortable hook wraps each item; arrayMove handles the reorder math; SortableContext coordinates the list.

tsx
Loading...

Remaining issues

  • No DragOverlay: The dragged item moves in-place using CSS transforms. If the parent has overflow: hidden, the dragged item clips. DragOverlay renders the preview in a portal at the document root, solving this.
  • No optimistic server sync: Calling onReorder which triggers a React Query mutation will cause the list to revert to server state until the mutation settles. Store local order in useState and update it immediately; rollback on error.
  • Clicks on interactive children: Without an activationConstraint on PointerSensor, a simple click on a button inside the row will trigger the drag. Add distance: 8 so a drag only starts after 8px of movement.

Production pattern

Combine DragOverlay for a portal-rendered preview, PointerSensor with activation constraint, KeyboardSensor for accessibility, local useState for optimistic reorder, and mutation rollback on error.

tsx
Loading...

Database ordering

If order is persisted as an integer column, inserting between two items requires renumbering siblings. Use fractional indexing instead (each item's order is a string like "a0" that sorts lexicographically) so any insert is a single-row update. Libraries like fractional-indexing handle this.

When I use this

  • Kanban boards: Card reorder within a column and cross-column moves (dnd-kit handles both with multiple SortableContext instances).
  • Block editors: Notion-style block reordering where any content block can be dragged above or below another.
  • Ordered lists with user preference: Saved search filters, dashboard widgets, playlist tracks.
  • Skip when: Items are sorted by a system criterion (date, score) the user can't override. Don't add drag interaction for its own sake.

Gotchas

  • Attach listeners to a handle, not the whole row. If you spread listeners on the <li>, every click-inside becomes a potential drag start. Use a <button> with a grip icon as the handle.
  • activationConstraint is not optional. Without distance: 8, clicking a button inside the row triggers the drag and swallows the click event. This causes confusing UX where buttons appear to not respond.
  • DragOverlay vs in-place transform. The in-place CSS transform approach is fine for simple cases. Use DragOverlay whenever the list sits inside a scrollable container, a modal, or anything with overflow: hidden.
  • dnd-kit + virtualized lists. There is a known incompatibility between @dnd-kit/sortable and TanStack Virtual: the virtualizer unmounts items that scroll out of view, which confuses the drop position calculation. The workaround is to overscan heavily during a drag (render more items outside the viewport) and reset overscan on drag end.
  • Rollback needs a snapshot. Capture items before the optimistic update (e.g. const prev = items) so you have something to roll back to in onError. Closures capture stale state; use a ref if the mutation is async.

Optimistic Updates → · Virtualized Lists → · Used in: Notion editor → · All patterns

Related Frameworks