webdevpuneet.com

Drag to Reorder Table Rows — HTML5 Drag and Drop Table

Drag to Reorder Table Rows · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="trd-card">
  <div class="trd-head">
    <h3>Sprint priority order</h3>
    <p class="trd-hint">Drag a row by its handle to reorder.</p>
  </div>
  <table class="trd-table">
    <thead><tr><th></th><th>#</th><th>Task</th><th>Owner</th><th>Points</th></tr></thead>
    <tbody id="trdBody"></tbody>
  </table>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real HTML5 draggable rows
Every tr uses native draggable="true" with dragstart/dragover/drop, not a mouse-position hack.
Correct preventDefault on dragover
Enables drop to fire at all, the most commonly missed step in native drag-and-drop.
Cursor-position drop indicator
A live line computed from getBoundingClientRect shows above/below placement before release.
Index-shift-safe reorder math
Splice-then-adjust logic correctly accounts for the dragged item's own removal shifting later indices.
Visual drag-lift feedback
The dragged row fades via a .dragging class so its origin position stays visible.
Array-driven re-render
The table always rebuilds from the ITEMS array, keeping the DOM and data order in sync.
Dedicated drag handle
A grip icon signals the draggable affordance without needing the whole row to look interactive.
Live row numbering
The # column renumbers automatically after every reorder, no manual bookkeeping.

About this UI Snippet

Drag to Reorder Table Rows — Real HTML5 Drag Events With a Drop Indicator

Screenshot of the Drag to Reorder Table Rows snippet rendered live

Ordered lists — a sprint backlog, a priority queue, a playlist — are far easier to reorganize by dragging a row where it belongs than by editing a rank number in a text field. This snippet implements genuine HTML5 drag-and-drop for table rows: real draggable="true" elements, dragstart/dragover/drop handlers, and a live drop-indicator line that shows exactly where the row will land before you release the mouse — not a fake reorder that just swaps two rows on click.

Native drag events, not mouse-position simulation

Every <tr> is marked draggable="true". On dragstart, the handler records which row index is being dragged (dragIndex), adds a .dragging class that fades the row to signal it's lifted, and calls e.dataTransfer.setData(...) — required by the drag-and-drop spec for some browsers to permit the drag at all, even though this implementation tracks the source index in a JS variable rather than reading it back out of dataTransfer.

dragover must call preventDefault, or drop never fires

By default, browsers don't allow dropping onto arbitrary elements — a drop event only fires on a target if its dragover handler called e.preventDefault(). This is the single most common HTML5 drag-and-drop bug: skip that line and the whole interaction silently does nothing. Here, every dragover on the tbody prevents the default and sets dropEffect = 'move' so the cursor shows the correct move icon.

A drop-indicator line computed from real cursor position

Rather than just highlighting the whole row you're hovering, dragover computes the cursor's vertical position relative to the hovered row's getBoundingClientRect() — if the cursor is in the top half, the indicator line is inserted above that row; in the bottom half, below it. This is a genuine positional calculation on every dragover event, not a static highlight, and it's what makes the drop location obvious before you let go.

Correct index math on drop

The trickiest part of any row-reorder is getting the final index right when the dragged row itself is removed from the array before being reinserted — removing an earlier row shifts every later index down by one. The drop handler splices the dragged item out first, then decrements insertAt by one if the target position was after the original position, before splicing it back in at the corrected index — the standard off-by-one trap in drag-reorder logic, handled explicitly here rather than by luck.

A real array as the source of truth

The whole table re-renders from the ITEMS array after every successful drop, so the DOM never drifts out of sync with the underlying order — dragging doesn't directly move DOM nodes around, it reorders the array and lets render() rebuild the table, keeping row numbering and any derived values consistent.

Customizing it

Persist the new order to a backend on drop, add multi-row drag selection, or combine with a column drag reorder for both axes. Compare with an editable table for inline field edits alongside reordering.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA five-row task list renders with a drag handle in the first column of each row.
  2. 2
    Press and drag a rowGrab the handle (or the row) — it fades to signal it's lifted, and the cursor becomes a grab icon.
  3. 3
    Watch the drop-indicator lineAs you drag over other rows, a blue line shows exactly above or below which row the drop will land.
  4. 4
    Release to dropThe row moves to its new position; the # column renumbers to reflect the new order.
  5. 5
    Drag to the very top or bottomDropping above the first row or below the last row places it at either end correctly.
  6. 6
    Swap in your own dataReplace the ITEMS array — render() and the drag handlers work for any row count.

Real-world uses

Common Use Cases

Sprint and backlog prioritisation
Let a team drag tasks into priority order, with a live drop line computed from getBoundingClientRect showing where the row will land.
Playlist and queue management
Reorder tracks or items in a queue, with preventDefault on dragover so the drop event fires at all.
Workflow step builders
Reorder steps in an onboarding or approval flow, using splice-then-adjust maths that accounts for the removed item's index shift.
Menu ordering
Let admins drag menu items into position, and compare with column dragging in table column drag reorder.
Native drag-and-drop learning
Study native draggable rows and the ranking use case, where manual order is clearer than editing a number field.

Got questions?

Frequently Asked Questions

The HTML5 drag-and-drop spec treats every element as a non-drop-target by default. Calling e.preventDefault() inside the dragover handler is the explicit signal that this element accepts the drop; skip it and the browser silently rejects the drop attempt, so the drop event handler never runs no matter how the drag ends.

Because removing the dragged row shifts the index of every row that came after it down by one. If you inserted at the raw target index without accounting for that shift, dropping a row just below its own original position would land it one slot further than intended. Decrementing insertAt when the target was after the original position corrects for this off-by-one.

In the drop handler, after ITEMS.splice(insertAt, 0, moved) and before or after render(), send the updated ITEMS array (or just the moved item's id and new index) to your backend with a fetch call, so the reordering survives a page reload rather than only existing in memory.

Native HTML5 drag events are unreliable on touch screens. For touch, implement the same drop-indicator and index-shift logic driven by touchstart/touchmove/touchend and pointer coordinates instead of the dragstart/dragover/drop events, or use the Pointer Events API (pointerdown/pointermove/pointerup) which unifies mouse and touch and can replace HTML5 DnD entirely if mobile support matters.

Keep ITEMS in component state and the same dragstart/dragover/drop handlers, but instead of mutating the array in place and calling render(), call your state setter with the new array (e.g. setItems(next) in React) after computing it with the same splice logic — the drag-position math and off-by-one correction are plain JavaScript and port unchanged.