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
dragoverhandler calledpreventDefault(). This is where you read data withgetData(). - 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
ondrophandler with a matchingondragoverhandler that callspreventDefault()— without it, drops are silently rejected. - Store a stable identifier (an
id) indataTransferrather 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
effectAllowedanddropEffectto 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
styleattributes — 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
divboxes labeled “To Do” and “Done”. Make a fewpelements 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 bygetData()inside theondrophandler 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, repeateddrag,dragenter, repeateddragover, then eitherdragleaveordrop, followed bydragendon the source. - The
DataTransferobject (event.dataTransfer) carries data from source to target viasetData()andgetData(). - A drop target must call
event.preventDefault()inside itsdragoverhandler, or thedropevent will never fire. effectAllowedanddropEffectcontrol 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
drophandler — the browser only manages the drag gesture and events.
