Published Sep 16, 2025 ⦁ Updated Oct 11, 2026 ⦁ 6 min read

Drag and Drop Interface: How to Build One for the Web

A drag and drop interface lets users move things by grabbing them with a mouse, finger or stylus and releasing them somewhere else: reordering a list, moving a card between columns, or dropping a file onto an upload area. On the web you build one in one of three ways: the browser's native HTML Drag and Drop API, your own logic on top of Pointer Events, or a library such as dnd-kit, Pragmatic drag and drop or SortableJS that handles the hard parts. This guide shows working code for each and covers the parts most tutorials skip: touch devices and accessibility.

Which approach to use

Approach Good for Weak at
Native HTML Drag and Drop API File drop zones, dragging between windows or apps, simple lists Custom drag visuals, consistent touch support, animation
Pointer Events (DIY) Canvases, sliders, custom editors, full control You build everything, including accessibility
Library (dnd-kit, Pragmatic, SortableJS) Sortable lists, kanban boards, trees, grids Adds a dependency and its API to learn

For a product feature such as a sortable list or a kanban board, use a library. For a file upload area, the native API is all you need.

Option 1: The native HTML Drag and Drop API

The browser fires dragstart, drag and dragend on the element being dragged, and dragenter, dragover, dragleave and drop on whatever is under the pointer (MDN: HTML Drag and Drop API). Two rules trip everyone up:

  1. Elements are not draggable by default (except images, links and selected text). Add draggable="true".
  2. Elements are not drop targets by default. You must call event.preventDefault() in dragover, or the drop event never fires.

Example: a sortable list in plain JavaScript

<ul id="tasks">
  <li draggable="true">Write tests</li>
  <li draggable="true">Fix login bug</li>
  <li draggable="true">Ship release</li>
</ul>
const list = document.getElementById("tasks");
let dragged = null;

list.addEventListener("dragstart", (e) => {
  dragged = e.target.closest("li");
  e.dataTransfer.effectAllowed = "move";
  e.dataTransfer.setData("text/plain", dragged.textContent); // some browsers need data set to start a drag
  dragged.classList.add("dragging");
});

list.addEventListener("dragover", (e) => {
  e.preventDefault(); // makes the list a valid drop target
  const over = e.target.closest("li");
  if (!over || over === dragged) return;
  const { top, height } = over.getBoundingClientRect();
  const after = e.clientY > top + height / 2;
  list.insertBefore(dragged, after ? over.nextSibling : over);
});

list.addEventListener("drop", (e) => e.preventDefault());

list.addEventListener("dragend", () => {
  dragged.classList.remove("dragging");
  dragged = null;
  const order = [...list.children].map((li) => li.textContent);
  console.log("new order", order); // persist it here
});

Add .dragging { opacity: 0.4; } so users can see what they're moving.

Example: a file drop zone

This is where the native API is the right tool, because only the browser can receive files dragged in from the operating system:

const zone = document.getElementById("dropzone");

zone.addEventListener("dragover", (e) => {
  e.preventDefault();
  zone.classList.add("over");
});
zone.addEventListener("dragleave", () => zone.classList.remove("over"));
zone.addEventListener("drop", (e) => {
  e.preventDefault();
  zone.classList.remove("over");
  for (const file of e.dataTransfer.files) {
    console.log(file.name, file.size, file.type);
  }
});

Always pair a drop zone with a normal <input type="file">. Keyboard users, screen reader users and many phone users can't drag files.

Limits of the native API

  • dataTransfer data can only be set in dragstart and only read in dragstart and drop. During dragover you can't see what's being dragged, only its types.
  • Making an element draggable interferes with selecting its text with the mouse.
  • Mobile support is uneven. As of October 2026, caniuse lists support in Safari on iOS 15 and later and in current Chrome for Android, but not in Firefox for Android or Samsung Internet (caniuse: Drag and Drop). If touch matters, use Pointer Events or a library.

Option 2: Pointer Events for full control

Pointer Events unify mouse, touch and pen input. You move the element yourself, which gives you complete control and works the same on every device:

const card = document.getElementById("card");
let startX, startY, originX, originY;

card.style.touchAction = "none"; // stop the browser from scrolling during the drag

card.addEventListener("pointerdown", (e) => {
  card.setPointerCapture(e.pointerId); // keep receiving events even outside the element
  startX = e.clientX;
  startY = e.clientY;
  originX = card.offsetLeft;
  originY = card.offsetTop;
});

card.addEventListener("pointermove", (e) => {
  if (!card.hasPointerCapture(e.pointerId)) return;
  card.style.left = originX + (e.clientX - startX) + "px";
  card.style.top = originY + (e.clientY - startY) + "px";
});

card.addEventListener("pointerup", (e) => {
  card.releasePointerCapture(e.pointerId);
  // hit-test drop targets here, e.g. document.elementsFromPoint(e.clientX, e.clientY)
});

(The card needs position: absolute inside a positioned container.) Hit-testing, auto-scrolling near edges, keyboard support and screen reader announcements are all up to you. That's why most teams pick a library.

Option 3: Use a library

Library Works with Notes (as of October 2026)
dnd-kit React, plus Vue, Svelte and Solid adapters Pointer, touch and keyboard sensors, built-in screen reader announcements. The common default for React.
Pragmatic drag and drop Any framework (vanilla TypeScript) Atlassian's library, built on the native API. Low-level building blocks for lists, trees, grids and resizing.
SortableJS Vanilla JS, with framework wrappers Quickest way to make an existing list sortable.
react-beautiful-dnd React Deprecated and archived by Atlassian. Don't use it for new work. The community fork @hello-pangea/dnd exists for existing apps.

Example: a sortable list with dnd-kit (React)

npm install @dnd-kit/react
import { useState } from "react";
import { DragDropProvider } from "@dnd-kit/react";
import { useSortable, isSortable } from "@dnd-kit/react/sortable";

function SortableItem({ id, index }) {
  const { ref } = useSortable({ id, index });
  return <li ref={ref}>{id}</li>;
}

export default function App() {
  const [items, setItems] = useState(["Write tests", "Fix login bug", "Ship release"]);

  return (
    <DragDropProvider
      onDragEnd={(event) => {
        if (event.canceled) return;
        const { source } = event.operation;
        if (isSortable(source) && source.initialIndex !== source.index) {
          setItems((prev) => {
            const next = [...prev];
            const [moved] = next.splice(source.initialIndex, 1);
            next.splice(source.index, 0, moved);
            return next;
          });
        }
      }}
    >
      <ul>
        {items.map((id, index) => (
          <SortableItem key={id} id={id} index={index} />
        ))}
      </ul>
    </DragDropProvider>
  );
}

This follows the pattern in the dnd-kit sortable guide. The older @dnd-kit/core and @dnd-kit/sortable packages you'll see in many tutorials use a different API. Check which one a project uses before copying code.

Accessibility: dragging can't be the only way

WCAG 2.2 success criterion 2.5.7, Dragging Movements (level AA), requires that anything you can do by dragging can also be done with a single pointer without dragging, for example by tapping or clicking (W3C: Understanding 2.5.7). In practice:

  • Give each item Move up and Move down buttons, or a "Move to…" menu for kanban columns.
  • Support the keyboard: focus an item, press Space to pick it up, use the arrow keys to move it, Space to drop and Escape to cancel. dnd-kit's keyboard sensor does this for you.
  • Announce changes in an aria-live region ("Fix login bug moved to position 1 of 3"). Don't use aria-grabbed or aria-dropeffect. They're deprecated in ARIA.

UX details that make drag and drop feel right

  • Show a clear handle (a grip icon) on items that can be dragged, especially when the item also has clickable content.
  • Preview the result while dragging: move the other items aside or show an insertion line, so the drop is never a surprise.
  • Highlight valid drop targets and show a "not allowed" state for invalid ones.
  • Auto-scroll when dragging near the edge of a scrollable container. Libraries handle this. With Pointer Events you have to build it.
  • Make it undoable. Save optimistically, and offer undo if the server rejects the change.

Drag and drop website builders

If you searched for drag and drop web development because you want to build a website by dragging blocks rather than coding a drag interaction, you want a visual builder instead. DevHunt lists these under no-code tools and visual editing tools. For prebuilt components, including draggable ones, see UI libraries.

FAQ

What is a drag and drop interface?

A UI where users move objects directly: grab an item, move it, release it on a target. Common examples are reordering lists, kanban boards like Trello and Jira, file upload areas, page builders, and dashboard widgets you can rearrange.

Does HTML5 drag and drop work on mobile?

Partly. Safari on iOS 15 and later and current Chrome for Android support it, but Firefox for Android and Samsung Internet don't, as of October 2026. For reliable touch support, use Pointer Events or a library with touch sensors, such as dnd-kit.

What's the best drag and drop library for React?

For most apps, dnd-kit: it's actively maintained and supports touch, keyboard and screen readers. Pick Pragmatic drag and drop if you need framework-agnostic, low-level control or cross-window dragging. Avoid react-beautiful-dnd, which is deprecated.

How do I make a drag and drop list accessible?

Offer a non-drag alternative (buttons or a menu), support keyboard pick-up, move and drop, and announce changes through an aria-live region. That covers WCAG 2.2 criterion 2.5.7 and makes the feature usable for keyboard and screen reader users.

Why doesn't my drop event fire?

You're not calling event.preventDefault() in the dragover handler of the drop target. Without it, the browser treats the element as an invalid target and never fires drop.