HTML Introduction to Drag and Drop

HTML includes a native Drag and Drop (DnD) API that lets you turn almost any element into something a user can pick up with the mouse and drop somewhere else on the page — without any external library. It powers everyday interactions like reordering a to-do list, dragging a file icon between folders, or moving a card between columns on a kanban board. The API is built directly into the browser: every drag gesture fires a predictable sequence of events, and a special DataTransfer object carries whatever data is being dragged from the source element to the drop target. This lesson covers the draggable attribute, the full set of drag events, and how to wire them together using inline event handler attributes.

Overview: How Drag and Drop Works

By default, most HTML elements are not draggable. A handful of elements are draggable automatically because dragging them is expected behavior: images (img), links (a with an href), and selected text. Everything else — a div, a li, a p, a span — only becomes draggable when you explicitly add the draggable attribute with a value of true.

Once an element is draggable, the browser takes over the low-level mechanics: it tracks the mouse, generates a translucent “ghost” preview of the element as the user drags it, and fires a series of events on both the element being dragged (the source) and whatever element the mouse is currently over (a potential target). Your job as the page author is to listen for those events and decide two things: what data the drag carries, and what should happen when it is dropped.

That data travels inside a DataTransfer object, which every drag event exposes as event.dataTransfer. You call setData(format, value) in the source element’s dragstart handler to store something (commonly the id of the dragged element, or plain text), and getData(format) in the target’s drop handler to retrieve it. This is what makes it possible to know, at drop time, exactly which element was dragged, even though the drop happens on a completely different element.

Crucially, most elements are not valid drop targets by default either. The browser’s built-in default behavior for a dragover event is to reject the drop. To make an element accept a drop, you must call event.preventDefault() inside its dragover handler (and often its dragenter handler too). This one detail trips up almost everyone the first time they build a drag-and-drop interface, and it is covered in more depth in Common Mistakes below.

Syntax

There is no single “drag and drop tag” — instead, the API is a combination of one attribute and several event handler attributes that you attach to ordinary elements.

<div
  draggable="true"
  ondragstart="event.dataTransfer.setData('text/plain', event.target.id)">
  Drag me
</div>

<div
  ondragover="event.preventDefault()"
  ondrop="event.preventDefault(); this.appendChild(document.getElementById(event.dataTransfer.getData('text/plain')))">
  Drop here
</div>
Attribute / Event Applies to Purpose
draggable Any element Set to true to make the element draggable, or false to explicitly disable native dragging (useful on images/links you don’t want draggable).
ondragstart The dragged (source) element Fires once, when the drag begins. Used to store data with event.dataTransfer.setData().
ondrag The dragged (source) element Fires repeatedly while the element is being dragged, similar to mousemove.
ondragenter Potential drop target Fires once when the dragged item first enters the target’s boundaries.
ondragover Potential drop target Fires repeatedly while the item hovers over the target. Must call preventDefault() here to allow a drop.
ondragleave Potential drop target Fires when the dragged item leaves the target without being dropped.
ondrop Drop target Fires when the item is released over the target. Used to read data with event.dataTransfer.getData() and update the DOM.
ondragend The dragged (source) element Fires on the source element once the drag operation finishes, whether or not the drop succeeded.

Examples

Example 1: A single draggable box and a drop zone

<div id='source' draggable='true'
     ondragstart="event.dataTransfer.setData('text/plain', event.target.id)">
  

Drag this box

</div> <div id='target' ondragover='event.preventDefault()' ondrop="event.preventDefault(); this.appendChild(document.getElementById(event.dataTransfer.getData('text/plain')))">

Drop zone

</div>

Result: The browser renders two block-level boxes stacked vertically, each containing a paragraph. The first box can be picked up with the mouse (the cursor changes and a translucent copy follows the pointer). Dragging it over the second box and releasing the mouse moves the entire #source div — including its “Drag this box” text — inside the #target div, right after the “Drop zone” paragraph.

The id on the source element is the piece of data that travels through dataTransfer. On drop, getData('text/plain') returns that id string, document.getElementById() looks the element up, and appendChild() physically relocates it in the DOM — the browser does not do this move automatically; your drop handler has to do it.

Example 2: A reorderable list

<ul id='task-list'>
  <li id='task-1' draggable='true'
      ondragstart="event.dataTransfer.setData('text/plain', event.target.id)">Write outline</li>
  <li id='task-2' draggable='true'
      ondragstart="event.dataTransfer.setData('text/plain', event.target.id)"
      ondragover='event.preventDefault()'
      ondrop="event.preventDefault(); this.parentNode.insertBefore(document.getElementById(event.dataTransfer.getData('text/plain')), this)">Record video</li>
  <li id='task-3' draggable='true'
      ondragstart="event.dataTransfer.setData('text/plain', event.target.id)"
      ondragover='event.preventDefault()'
      ondrop="event.preventDefault(); this.parentNode.insertBefore(document.getElementById(event.dataTransfer.getData('text/plain')), this)">Publish lesson</li>
</ul>

Result: A bulleted list with three items is rendered: “Write outline”, “Record video”, “Publish lesson”. Because every li is both draggable and a drop target, dragging any item onto another reorders the list — the dragged item is inserted immediately before whichever item it was dropped on.

Notice each item’s ondrop handler uses insertBefore() instead of appendChild(). This is the same core pattern as Example 1, but applied item-by-item so items can be freely reordered rather than only moved into one fixed container.

Example 3: Restricting what can be dropped

<div id='photo' draggable='true'
     ondragstart="event.dataTransfer.setData('text/plain', 'photo'); event.dataTransfer.effectAllowed='copy'">
  

Profile photo

</div> <div id='avatar-slot' ondragover="if (event.dataTransfer.types.includes('text/plain')) { event.preventDefault(); event.dataTransfer.dropEffect='copy'; }" ondrop="event.preventDefault(); this.textContent = 'Photo accepted: ' + event.dataTransfer.getData('text/plain')">

Avatar slot

</div>

Result: Two boxes are rendered, “Profile photo” and “Avatar slot”. Dragging the photo box over the avatar slot shows a “copy” style cursor (a small plus icon in most browsers) because dropEffect is set to copy. Releasing the mouse replaces the avatar slot’s text with “Photo accepted: photo”.

This example shows that dataTransfer carries more than a single string: effectAllowed (set on the source) and dropEffect (set on the target) together control which cursor icon the browser shows, communicating to the user whether the drop will copy, move, or link the item.

How It Works Step by Step

When a user drags an element from source to target, the browser fires this exact sequence:

  • dragstart — fired once on the source element the moment the mouse starts moving after being pressed down on a draggable element. This is where you call setData().
  • drag — fired repeatedly on the source element throughout the drag, roughly matching mouse movement.
  • dragenter — fired on an element the moment the dragged item’s pointer crosses into its boundaries.
  • dragover — fired repeatedly on whatever element is currently under the pointer. If nothing calls preventDefault() here, the browser’s default action runs instead (which rejects the drop).
  • dragleave — fired on an element when the dragged item’s pointer exits its boundaries without a drop occurring.
  • drop — fired on the element the pointer is over when the mouse button is released, but only if that element’s dragover handler called preventDefault(). This is where you read data with getData().
  • dragend — fired on the original source element after the operation completes, regardless of whether the drop succeeded.

Internally, none of this changes the DOM by itself. The browser only handles the visual dragging gesture and the event dispatch; every actual DOM mutation — moving a node, updating text, adding a class — happens because your event handler code does it, typically inside ondrop.

Common Mistakes

Mistake 1: Forgetting to prevent the default on dragover

The single most common bug is a drop target that never receives a drop event:

<div id='target' ondrop="this.textContent='Dropped!'">
  Drop zone
</div>

This markup is well-formed, but it will never work: without a dragover handler that calls event.preventDefault(), the browser’s default behavior treats the element as “not a valid drop target” and the drop is rejected before your ondrop handler ever runs. The fix is to add ondragover='event.preventDefault()' as well:

<div id='target'
     ondragover='event.preventDefault()'
     ondrop="event.preventDefault(); this.textContent='Dropped!'">
  Drop zone
</div>

Mistake 2: Using an invalid value for draggable

<li draggable='yes'>Move me</li>

draggable only recognizes the exact string values true or false. A value like 'yes' is not one of the recognized enumerated values, so the browser falls back to the element’s default draggable state — which is false for a li. The element silently fails to drag, with no error to warn you. The fix is to always use the literal string true:

<li draggable='true'>Move me</li>

Best Practices

  • Always pair a target’s ondrop handler with a matching ondragover handler that calls preventDefault() — without it, drops are silently rejected.
  • Store a stable identifier (an id) in dataTransfer rather than relying on which element currently has focus or is “selected” — the drop handler runs on a different element than the one that started the drag.
  • Set effectAllowed and dropEffect to give users a visual cursor cue about whether a drop will move, copy, or link the item.
  • Don’t rely on drag and drop as the only way to perform an action (like reordering a list) — keyboard users and touch-screen users cannot use the native mouse-drag gesture, so provide an alternative such as up/down buttons.
  • Keep visual styling (borders, highlight colors for a valid drop zone, cursor changes) in CSS, not inline style attributes — this keeps markup focused on structure and behavior.
  • Explicitly set draggable='false' on images or links you do not want the user to accidentally drag, since those elements are draggable by default.

Practice Exercises

  • Exercise 1: Build two side-by-side div boxes labeled “To Do” and “Done”. Make a few p elements draggable inside “To Do”, and wire up “Done” so dropping a paragraph on it moves that paragraph into the “Done” box.
  • Exercise 2: Take the reorderable list from Example 2 and extend it to five items. Verify that dragging the last item onto the first one moves it to the top of the list.
  • Exercise 3: Modify Example 3 so the avatar slot only accepts the drop if the dragged item’s id is exactly 'photo', and shows different text (for example, “Rejected”) if some other draggable element is dropped on it. Hint: check the value returned by getData() inside the ondrop handler before updating the DOM.

Summary

  • Most elements need draggable='true' to become draggable; images, links, and selected text are draggable by default.
  • A full drag operation fires, in order: dragstart, repeated drag, dragenter, repeated dragover, then either dragleave or drop, followed by dragend on the source.
  • The DataTransfer object (event.dataTransfer) carries data from source to target via setData() and getData().
  • A drop target must call event.preventDefault() inside its dragover handler, or the drop event will never fire.
  • effectAllowed and dropEffect control the cursor icon shown during a drag, signaling copy, move, or link.
  • All DOM changes on drop (moving, inserting, or updating elements) must be done manually inside your drop handler — the browser only manages the drag gesture and events.