Definition
Use this API when you want to build "pick up this item and drag it over there," but the source and target span different lists/regions (across React component boundaries). Because the browser manages the drag directly, moves that cross boundaries become easy.
HTML5 Drag and Drop is a native drag session (a real drag state managed directly by the operating system) API provided by the browser. It moves the payload (the actual data you're moving) via the dragstart → dragover → drop / dragend events and DataTransfer (a browser-built-in container that holds the data to move during a drag), and only works once the pointer enters the OS drag loop. In many cases, a mouse event synthesized (imitated in code) by a framework alone will not start the session.
Why it matters
The UX of "move a selected region somewhere else" is common in editors, lists, and file uploads. When React or an inline editor owns only one subtree, the framework's internal DnD can't cross the boundary. HTML5 DnD works across the entire DOM tree, so it becomes the channel to move text/nodes to a target outside the focused subtree.
How it works
- When the user drags a selected region,
dragstartfires, and the handler puts atext/plain(etc.) payload intodataTransferwith a MIME type (a label indicating the data format, e.g., plain text/HTML). dragoverrepeats while the pointer moves. To allow dropping, the target needspreventDefault().- In
drop, read the payload, compute the insertion position withcaretRangeFromPoint(a browser function that takes screen coordinates and returns the text-cursor position at that point), etc., then apply it to domain state. dragendends the session.img/aaredraggableby default, so they can trigger an unintended native drag.
| Item | Description | Caveat |
|---|---|---|
dragstart | Start session, set payload | Text drag requires an existing selection |
dragover | preventDefault() to allow drop | Without it, drop never fires |
drop | Target receives the payload | Computing the insertion position is key |
DataTransfer | Payload per MIME type | Serialization differs across browsers |
draggable | Declare the drag source | Set decorative elements to false |
Practical application
If you need a cross-subtree move, use the HTML5 channel instead of framework DnD, and reflect the drop result as a domain patch (atomic placement). On drag start, tidy up the focus/editor session to remove the source-of-truth race.
For E2E, don't assume the OS drag loop; verify the path by synthetically dispatching the DragEvent the app listens for. Playwright's synthetic mouse often can't enter the OS loop, so dragstart frequently doesn't fire.
Trade-offs
- The native API is strong for cross-subtree/file drops, but there are per-browser differences in
DataTransfer, cursor, and accessibility. - If a React re-render swaps the source/target DOM during a drag, the session breaks. During a drag, minimize re-renders or pin to a static render.
When not to use
- When you only need simple reordering within the same subtree, and pointer events + state updates are enough.
- When touch-only UX is the main body — HTML5 DnD's touch support is limited depending on the environment. Consider a Pointer Events (low-level input events that unify mouse/touch/pen) based alternative.
Common mistakes
- Omitting
preventDefault()indragover, sodropnever comes. - Images/links/handle glyphs intercepting the text drag (
draggable={false}/ pseudo-element not applied). - E2E assuming OS drag and failing only in CI.
- Not distinguishing whether the drop coordinate points at the contenteditable or the static render.
Related concepts
- selection-tostring-and-pseudo-content — often handled together with the problem of handle glyphs bleeding into selection/copy.
- focused-guest-editor — when a guest owns only one block, HTML5 DnD is often chosen as the cross-block move channel.