Drag & Drop Reorder List — Free HTML CSS JS Sortable List Snippet
Drag & Drop Reorder List · Layouts · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<ul class="reorder-list" id="reorderList">
<li class="item" draggable="true" data-id="1">
<span class="handle" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="8" cy="6" r="1.6"/><circle cx="16" cy="6" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="18" r="1.6"/><circle cx="16" cy="18" r="1.6"/></svg>
</span>
<span class="label">Write project proposal</span>
</li>
<li class="item" draggable="true" data-id="2">
<span class="handle" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="8" cy="6" r="1.6"/><circle cx="16" cy="6" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="18" r="1.6"/><circle cx="16" cy="18" r="1.6"/></svg>
</span>
<span class="label">Review pull requests</span>
</li>
<li class="item" draggable="true" data-id="3">
<span class="handle" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="8" cy="6" r="1.6"/><circle cx="16" cy="6" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="18" r="1.6"/><circle cx="16" cy="18" r="1.6"/></svg>
</span>
<span class="label">Schedule design review</span>
</li>
<li class="item" draggable="true" data-id="4">
<span class="handle" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="8" cy="6" r="1.6"/><circle cx="16" cy="6" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="18" r="1.6"/><circle cx="16" cy="18" r="1.6"/></svg>
</span>
<span class="label">Update onboarding docs</span>
</li>
<li class="item" draggable="true" data-id="5">
<span class="handle" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="8" cy="6" r="1.6"/><circle cx="16" cy="6" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="18" r="1.6"/><circle cx="16" cy="18" r="1.6"/></svg>
</span>
<span class="label">Ship v2.3 release</span>
</li>
</ul>* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #f8fafc; padding: 32px; margin: 0; display: flex; align-items: center; flex-direction: column; gap: 20px; justify-content: center; min-height: 100vh; }
.reorder-list {
list-style: none;
margin: 0;
padding: 0;
max-width: 380px;
display: flex;
flex-direction: column;
gap: 8px;
}
.item {
display: flex;
align-items: center;
gap: 10px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 12px 14px;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
cursor: grab;
transition: box-shadow 0.15s, border-color 0.15s;
}
.item:active { cursor: grabbing; }
.item.dragging {
opacity: 0.4;
}
.item.drag-over-top {
border-top: 2px solid #6366f1;
}
.item.drag-over-bottom {
border-bottom: 2px solid #6366f1;
}
.handle {
color: #cbd5e1;
display: flex;
flex-shrink: 0;
}
.label {
font-size: 14px;
color: #1e293b;
font-weight: 500;
}const list = document.getElementById('reorderList');
let draggedItem = null;
list.addEventListener('dragstart', (e) => {
draggedItem = e.target.closest('.item');
draggedItem.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
list.addEventListener('dragend', () => {
if (draggedItem) draggedItem.classList.remove('dragging');
draggedItem = null;
clearIndicators();
});
list.addEventListener('dragover', (e) => {
e.preventDefault();
const target = e.target.closest('.item');
if (!target || target === draggedItem) return;
clearIndicators();
const rect = target.getBoundingClientRect();
const isAfter = e.clientY - rect.top > rect.height / 2;
target.classList.add(isAfter ? 'drag-over-bottom' : 'drag-over-top');
});
list.addEventListener('drop', (e) => {
e.preventDefault();
const target = e.target.closest('.item');
if (!target || target === draggedItem) return;
const rect = target.getBoundingClientRect();
const isAfter = e.clientY - rect.top > rect.height / 2;
target.insertAdjacentElement(isAfter ? 'afterend' : 'beforebegin', draggedItem);
clearIndicators();
});
function clearIndicators() {
list.querySelectorAll('.item').forEach((item) => {
item.classList.remove('drag-over-top', 'drag-over-bottom');
});
}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.
What's included
Features
About this UI Snippet
Drag & Drop Reorder List — HTML, CSS & JavaScript Sortable List

Task lists, playlists, and priority queues often need to be manually reordered by the user. Rather than reaching for a sortable-list library, the browser already ships everything needed for this: the native HTML5 Drag and Drop API, available on any element with a draggable="true" attribute.
This snippet builds a fully working reorderable list using only that native API plus a handful of DOM events — dragstart, dragover, drop, and dragend — with no external sortable library.
How dragging an item works
Every <li class="item"> has draggable="true". When a drag begins, the dragstart listener (attached once, to the list itself, using event delegation via e.target.closest('.item')) records the dragged element in a draggedItem variable and adds a .dragging class that fades it to 40% opacity, giving clear visual feedback about which item is currently being moved.
How the insertion indicator works
The interesting part is figuring out *where* to drop the item relative to the row currently under the cursor. On every dragover event, the code calls target.getBoundingClientRect() to get the hovered row's position, then compares e.clientY (the cursor's vertical position) against the row's vertical midpoint: e.clientY - rect.top > rect.height / 2. If the cursor is in the bottom half of the row, a .drag-over-bottom class draws a colored line under it; if it's in the top half, .drag-over-top draws the line above it. This gives the user a precise, continuously updating preview of exactly where the item will land before they release the mouse.
How the actual reorder happens
The drop handler repeats the same top/bottom midpoint check, then uses the native insertAdjacentElement('beforebegin' | 'afterend', draggedItem) to physically move the dragged <li> in the DOM relative to the target row. Because insertAdjacentElement moves an existing node rather than cloning it, there's no need to remove-and-reinsert manually or worry about losing event listeners attached to the item.
Reading the list order afterward
To persist the new order, iterate list.querySelectorAll('.item') after any drop and read each element's data-id in document order — that gives you the array to send to your backend or save to local storage.
Step by step
How to Use
- 1Load the snippetClick "Drag & Drop Reorder List" in the sidebar Library tab. The preview shows five draggable task items.
- 2Drag an itemPress and hold an item, then drag it up or down. Watch the indigo insertion line show exactly where it will land.
- 3Drop itRelease the mouse to drop the item in its new position — the list reorders immediately with no page reload.
- 4Add more itemsIn the HTML panel, copy an existing <li class="item" draggable="true"> block and give it a unique data-id — no JS changes are needed.
- 5Persist the new orderIn the JS panel, add a function that reads querySelectorAll('.item') after each drop and saves the resulting data-id order to your backend or localStorage.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for React, or "Tailwind" for React + Tailwind CSS.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
No. It is built entirely on the browser's native HTML5 Drag and Drop API — the draggable attribute plus dragstart, dragover, drop, and dragend events. No external library is loaded.
On every dragover event, the code compares the cursor's vertical position (e.clientY) against the hovered row's vertical midpoint, calculated from getBoundingClientRect(). Above the midpoint inserts before the row; below it inserts after.
Browsers do not allow dropping on an element by default — calling preventDefault() in dragover is required to signal that this element is a valid drop target, and calling it in drop prevents the browser's default action (like navigating to a dragged link).
After any drop, call list.querySelectorAll('.item') and map each element's data-id attribute in document order — that array reflects the current order and can be sent to your backend or saved to localStorage.
The native HTML5 Drag and Drop API has inconsistent touch support across mobile browsers. For a touch-friendly version, use Pointer Events (pointerdown/pointermove/pointerup) instead, tracking position manually rather than relying on native drag events.
Yes. Remove draggable="true" from the .item and add it to the .handle span instead, then adjust dragstart to walk up to the parent .item with closest('.item') as it already does.
Fading with opacity: 0.4 rather than hiding it entirely keeps the list layout stable and gives the user a persistent visual anchor for which item they are moving, matching the behavior users expect from native OS drag interactions.
Yes, using the FLIP technique: record each item's bounding rect before the reorder, perform the DOM move, then read the new positions and animate from the old position to the new one with a CSS transform transition.