Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Comparison

Drag and Drop in React: Pick the Library, Not the Fight

Sean

Platform Writer

Aug 30, 2026
8 min read

The native HTML5 drag and drop API works on desktop, ignores touch entirely, and has an event model that fights you. For anything users will actually use, take a library.

Drag and Drop in React: Pick the Library, Not the Fight

Drag and drop looks like a small feature and is not. Between pointer events, touch, keyboard accessibility, scroll containers, and persisting the new order to a server, a kanban board is a week of work if you start from the platform API.

This covers what the native API does badly, what the library ecosystem looks like now, and the part everyone underestimates: saving the result.

Table of contents

What is wrong with the native API

HTML5 drag and drop exists and is genuinely usable for one narrow case: dragging files in from the desktop. For reordering elements within your own UI, it has problems that are not stylistic.

  • No touch support. dragstart does not fire on touch devices. Your feature simply does not exist on mobile.
  • The drag image is barely controllable. setDragImage exists and behaves differently in every browser.
  • dragover must call preventDefault() or the drop never fires. This is the single most common reason drag and drop ‘does not work’.
  • The dataTransfer object is write-only during drag. You cannot read what is being dragged until the drop, which makes live validation impossible.
  • No keyboard accessibility at all.

That last one is disqualifying for most production work. A reorder control that cannot be operated without a mouse excludes users and, in many contexts, fails an accessibility requirement outright.

For file uploads, though, the native API is right and a library is overkill:

<div
  onDragOver={(e) => e.preventDefault()}
  onDrop={(e) => {
    e.preventDefault();
    handleFiles([...e.dataTransfer.files]);
  }}
>
  Drop files here
</div>

The library landscape

The ecosystem has consolidated. In rough terms:

  • dnd-kit — the current default for new projects. Built on pointer events so touch works, keyboard accessible out of the box, no wrapper elements forced into your DOM, and a framework-agnostic core with a React adapter.
  • react-beautiful-dnd — excellent for vertical and horizontal lists, with the best-feeling animations of any of them. No longer actively developed, which matters for a long-lived project.
  • react-dnd — the flexible, lower-level option built around a backend abstraction. Powerful, and more machinery than most applications need.
  • Native HTML5 — files only.

For a new list-reordering or kanban feature, dnd-kit is the reasonable default. The pointer-event foundation means one implementation serves mouse, touch and pen, which is the thing that makes the others expensive.

A sortable list, minimally

The shape of the thing, so you can judge the ergonomics:

import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, useSortable, arrayMove,
         verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

function Row({ id, children }) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
  return (
    <li
      ref={setNodeRef}
      style={{ transform: CSS.Transform.toString(transform), transition }}
      {...attributes}
      {...listeners}
    >
      {children}
    </li>
  );
}

function TaskList({ tasks, onReorder }) {
  return (
    <DndContext
      collisionDetection={closestCenter}
      onDragEnd={({ active, over }) => {
        if (!over || active.id === over.id) return;
        const from = tasks.findIndex((t) => t.id === active.id);
        const to = tasks.findIndex((t) => t.id === over.id);
        onReorder(arrayMove(tasks, from, to));
      }}
    >
      <SortableContext items={tasks} strategy={verticalListSortingStrategy}>
        <ul>{tasks.map((t) => <Row key={t.id} id={t.id}>{t.title}</Row>)}</ul>
      </SortableContext>
    </DndContext>
  );
}

Two things that catch people. The id values must be stable and unique — array indices break the moment anything moves, which is the entire point of the feature. And over is null when the drop lands outside a valid target, so the guard is required rather than defensive.

Accessibility, which the library gives you for free

Keyboard support is what separates a real implementation from a demo. With dnd-kit, the keyboard sensor handles it: tab to the item, space to lift, arrows to move, space to drop, escape to cancel.

What you should still supply is announcements, so a screen reader user knows what happened:

<DndContext
  accessibility={{
    announcements: {
      onDragStart: ({ active }) => `Picked up ${active.id}.`,
      onDragOver:  ({ active, over }) => over ? `${active.id} over ${over.id}.` : '',
      onDragEnd:   ({ active, over }) => over ? `${active.id} dropped on ${over.id}.` : 'Cancelled.',
    },
  }}
>

Use meaningful names rather than IDs in those strings — Picked up Review the invoice is useful; Picked up 7f3a-11 is not.

Test it by unplugging the mouse and completing a reorder. It takes a minute and it is the only test that actually verifies this works.

Persisting the order, which is the actual hard part

The UI is the easy half. The order has to survive a refresh, which means a column in a database and an endpoint to update it. Three approaches, in increasing order of sense:

Integer positions, renumbered on every change. Simple and wrong at scale: moving one item rewrites every row after it, and two concurrent reorders produce a mess.

Sparse integers — 1000, 2000, 3000. Inserting between two items uses the midpoint, so most moves update one row. Eventually the gap closes and you renumber, which is a background job nobody remembers to write.

Fractional or lexicographic ranks. Store a string or decimal that always has room between any two values. This is what collaborative tools use, it never needs renumbering, and there are small libraries that generate the keys.

Whichever you choose, update optimistically and reconcile:

async function onReorder(next) {
  const previous = tasks;
  setTasks(next);                     // instant feedback
  try {
    await api.reorder(next.map((t, i) => ({ id: t.id, position: i })));
  } catch (err) {
    setTasks(previous);               // roll back on failure
    toast.error('Could not save the new order');
  }
}

Waiting for the server before moving the item makes drag and drop feel broken, because the interaction is inherently direct. Optimistic update with a rollback is the correct pattern here.

The endpoint behind it

Reordering means a write per drag, and users drag a lot — often several times in a few seconds while arranging things. Debounce on the client so a burst of moves becomes one request, and make the endpoint idempotent so a retry cannot corrupt the order.

It is also worth sending the whole affected ordering rather than a diff. A diff is smaller and considerably harder to reason about when two people reorder the same list at once.

That API and the database behind it deploy on RunxBuild from a repository — a Node, Python or Go service with a build log, a live route and rollback to the previous deploy, and a managed Postgres or MySQL holding the positions, with backups and connection limits handled.

How this fits the rest of the stack

Use a pointer-event library rather than the native API, because touch and keyboard support are not optional and are not things you should build twice. Then spend your attention on persistence: stable IDs, a rank scheme that does not need renumbering, optimistic updates with rollback, and a debounced idempotent endpoint. The API and database behind that are the parts with a monthly cost, and the RunxBuild hosting calculator shows them together.

Useful related references:

FAQ

What is the best drag and drop library for React?

dnd-kit is the reasonable default for new projects — it is built on pointer events so touch works, it is keyboard accessible out of the box, and it does not force wrapper elements into your DOM. react-beautiful-dnd feels excellent for lists but is no longer actively developed.

Why does my HTML5 drag and drop not work?

Almost always because dragover does not call preventDefault(). Without it the browser rejects the drop and the drop event never fires. The other common cause is expecting it to work on touch devices, where dragstart does not fire at all.

Does drag and drop work on mobile?

Not with the native HTML5 API, which has no touch support. Libraries built on pointer events, such as dnd-kit, handle mouse, touch and pen with one implementation, which is the main practical reason to use one.

How do I make drag and drop keyboard accessible?

Use a library with a keyboard sensor — dnd-kit provides lift, move, drop and cancel via space and arrow keys by default. Add screen reader announcements describing what was picked up and where it landed, using item names rather than IDs.

How should I store the order in a database?

Avoid renumbering every row on each change. Sparse integers with periodic renumbering work for small lists; fractional or lexicographic rank strings never need renumbering and handle concurrent reorders better. Update optimistically on the client and roll back if the request fails.

#drag and drop react#dnd-kit#React UI#HTML5 drag and drop#React