Practical Drag-to-Reorder List
Build a reorderable list with the Drag and Drop API.
Practical Drag-to-Reorder List is a free HTML Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the HTML Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Pattern Overview
To build a drag-to-reorder list, make every item draggable, store the dragged item's id in dataTransfer on dragstart, and on drop insert the dragged element before or after the target. The DOM and model stay in sync if you rebuild the order from the DOM after each drop.
Markup
Render a flat list where every li carries draggable="true" and a data-id. Keeping ids on the elements means the drag handlers do not need to query a separate state store to identify items — the element itself is the source of truth.
<ul id="list">
<li draggable="true" data-id="1">Item 1</li>
<li draggable="true" data-id="2">Item 2</li>
<li draggable="true" data-id="3">Item 3</li>
</ul>dragstart Records the Source
On dragstart, store the dragged id in dataTransfer and apply a "dragging" class for visual feedback. effectAllowed="move" tells the browser this is a reorder rather than a copy, so the cursor and dropEffect reflect a move operation.
list.addEventListener("dragstart", (e) => {
const li = e.target.closest("li");
if (!li) return;
e.dataTransfer.setData("text/plain", li.dataset.id);
e.dataTransfer.effectAllowed = "move";
li.classList.add("dragging");
});dragover Allows the Drop
Listen on the list (event delegation), call preventDefault, and set dropEffect to "move". Use e.target.closest("li") to find the hovered row so the same handler works for the whole list.
Computing Insert Position
To decide whether the dragged item should land above or below the hovered row, compare the cursor Y to the row's vertical midpoint: const before = (e.clientY - rect.top) < rect.height / 2. before=true means insertBefore, false means insertAfter.
function getInsertBefore(target, clientY) {
const rect = target.getBoundingClientRect();
return clientY < rect.top + rect.height / 2;
}drop Reorders the DOM
In the drop handler, look up the dragged element by id and the target row under the cursor. Call list.insertBefore(dragged, target) or list.insertBefore(dragged, target.nextSibling) depending on which half of the target was hovered.
dragend Cleans Up Styles
Whether the drop succeeded or the user pressed Escape, dragend always fires on the source. Use it to remove the "dragging" class and any drop-target highlights so the list returns to its idle state.
list.addEventListener("dragend", () => {
list.querySelectorAll(".dragging, .over").forEach((el) => {
el.classList.remove("dragging", "over");
});
});Visual Drop Indicator
Show a thin line where the item will land using a CSS pseudo-element on the hovered row, toggled by an "over-top" or "over-bottom" class that the dragover handler sets based on the cursor position. The user sees the insertion point before releasing.
Syncing the Model
After drop, derive the new order from the DOM: const order = Array.from(list.children).map((li) => li.dataset.id). Send that array to the server or store it in state — never try to maintain a parallel index alongside the DOM, which drifts under fast drags.
Keyboard Alternative
Add Up/Down handlers that move the focused item with insertBefore. Announce the move via an aria-live region ("Item moved up to position 2"). Keyboard users get the same capability without engaging the visual drag, which is essential for accessibility.
Touch Friendly Fallback
For mobile, swap the native drag for Pointer Events: on pointerdown capture the row, on pointermove translate it with CSS transform, on pointerup hit-test the underlying row and reorder. Libraries like SortableJS encapsulate this pattern across input types.
Knowledge Check
Why do we compute insertion position from the cursor's position relative to the hovered row's vertical midpoint?
Summary
A robust reorder list combines draggable="true" + dragstart (record id, effectAllowed=move) + dragover (preventDefault + indicator position) + drop (insertBefore at the midpoint-derived slot) + dragend (cleanup). Always derive the new order from the DOM after the drop and ship a keyboard fallback for accessibility.
Frequently asked questions
Is the “Practical Drag-to-Reorder List” lesson free?
Yes — the full text of “Practical Drag-to-Reorder List” is free to read here on the web, and the HTML Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the HTML Academy course, upgrade to CoddyKit PRO.
What will I learn in “Practical Drag-to-Reorder List”?
Build a reorderable list with the Drag and Drop API. You practise HTML Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start HTML Academy?
No prior experience is required. HTML Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Practical Drag-to-Reorder List” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this HTML Academy lesson?
Yes. Every HTML Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- draggable Attribute and dragstart Event
- dragover and drop Events
- dataTransfer Object
- Practical Drag-to-Reorder List