Drag to Sort List — Free HTML CSS JS Snippet

Drag to Sort List · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

HTML5 draggable="true" + dragstart/dragover/drop/dragend events — no library
dragSrc stores reference to dragged element across event boundary
Mouse Y vs item midY determines insert before/after position
dragover clears all .drag-over before setting new target — no stale highlights
.dragging: opacity:0.5 on the source item during drag
.drag-over: indigo border + subtle background on hover target
showOrder(): reads DOM order after each drop for order display
e.dataTransfer.effectAllowed = "move" prevents copy cursor on drop

About this UI Snippet

Drag to Sort List — HTML5 Drag-and-Drop Reordering, Drop Position by Mouse Y & Order Display

Screenshot of the Drag to Sort List snippet rendered live

A sortable drag-and-drop list lets users reorder items by dragging — a common pattern in task management apps (the column-based kanban board is the multi-list cousin), priority queues, settings pages, and any interface where order matters to the user. This snippet implements a complete drag-to-sort list using only the HTML5 Drag and Drop API: no SortableJS, no jQuery UI, no library. Drag an item, see an indigo drop-target indicator, and release to reorder — the new order displays below.

The HTML5 Drag and Drop API

Each list item has draggable="true". The dragstart event stores the dragged item in dragSrc and adds .dragging (opacity: 0.5). dragover on each item fires continuously while dragging over it — e.preventDefault() is required to allow the drop. dragend fires when the drag completes (drop or cancel) and cleans up all visual states.

Mouse Y position for drop before/after

The drop handler computes where to insert: const rect = item.getBoundingClientRect(); const midY = rect.top + rect.height / 2. If e.clientY < midY (cursor in the top half), insert before; otherwise insert after. This makes the drop position feel natural — the item appears where the cursor is pointing, not always at the same position relative to the target.

The drag-over indicator

All .drag-over classes are cleared on each dragover event before the new target gets its class. This prevents stale highlights. The .drag-over class adds an indigo border and subtle background tint — a clear visual signal of where the item will drop.

The order display

After each drop, showOrder() reads the current DOM order of .item-title elements and displays a numbered list below the sortable list. In production, read the reordered IDs and POST to your API: const ids = [...items].map(el => el.dataset.id).

Ghost image note

The browser creates a ghost image of the dragged element during drag. For a custom ghost, use e.dataTransfer.setDragImage(customEl, 0, 0) in dragstart. The ghost is automatically cleaned up after dragend.

Touch support

The HTML5 Drag and Drop API does not work on touch screens. For touch support, use the pointer events API: pointerdown records startY, pointermove updates transform: translateY, pointerup calculates the drop position and moves the DOM element.

Serialising the new order

After each drop, serialise the current DOM order to an array of IDs for your API. Add a data-id attribute to each .sort-item: <li class="sort-item" draggable="true" data-id="task-123">. In showOrder() or the dragend handler: const ids = [...list.querySelectorAll(".sort-item")].map(el => el.dataset.id); fetch("/api/reorder", { method: "PATCH", body: JSON.stringify({ ids }) }). This is the standard pattern for persisting sort order to any backend.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the five drag events yourself, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why e.preventDefault() inside the dragover handler is required for the drop event to ever fire, and how comparing e.clientY against an item's midpoint (rect.top plus half its height) decides whether insertBefore targets the item itself or its nextSibling. The same assistant can help you optimize it, for instance checking whether clearing every .drag-over class on every single dragover firing across all items is wasteful compared to only touching the previous and current target. It's also useful for extending the list: ask it to make only the drag-handle icon draggable instead of the whole row, add a brief highlight animation on the item that just moved, or wire showOrder() to POST the reordered IDs to a real backend endpoint. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a drag-and-drop reorderable list in plain HTML, CSS, and JavaScript using only the native HTML5 Drag and Drop API — no SortableJS, no other library.

Requirements:
- A list of items, each with the draggable="true" attribute, where dragstart stores a reference to the dragged element in an outer variable and adds a visual dragging class (reduced opacity) to it, and sets dataTransfer.effectAllowed to "move".
- A dragover handler on every item that calls e.preventDefault() (required for the drop event to ever fire) and, only when the hovered item is not the dragged item itself, clears any existing drop-target highlight class from all items before adding it to the currently hovered item, so exactly one item is ever highlighted as the drop target at a time.
- A drop handler that computes the target item's vertical midpoint from its bounding rect, and inserts the dragged element before the target if the drop's clientY is above that midpoint, or after the target (before its nextSibling) if below it, so the drop position feels like it follows the cursor rather than always landing in a fixed spot relative to the target.
- A dragend handler that removes the dragging class from the source item and every drop-target highlight class from the whole list, regardless of whether the drag ended in a successful drop or was cancelled.
- After every successful reorder, read the current DOM order of the list items and update a visible readout showing the new sequence, structured so it would be trivial to instead serialize item IDs and send them to a backend endpoint to persist the order.

Step by step

How to Use

  1. 1
    Drag any item by its handle or anywhere on the rowThe dragged item dims to 50% opacity. An indigo border appears on the item where you are hovering. Release to drop — the item inserts before or after the target based on where your cursor is vertically.
  2. 2
    See the new order displayed belowAfter each drop, the current order of item titles appears below the list. In production, read the DOM order to get the reordered IDs and sync to your backend.
  3. 3
    Add data-id attributes for backend syncAdd data-id="123" to each .sort-item element. After a drop, collect IDs in order: const ids = [...list.querySelectorAll(".sort-item")].map(el => el.dataset.id). POST this array to your API to persist the new order.
  4. 4
    Make only the drag handle draggableRemove draggable="true" from the .sort-item. Add it only to the .drag-handle: <div class="drag-handle" draggable="true">. Listen for dragstart on the handle and traverse to the parent li to get the item reference.
  5. 5
    Add animation on reorderAdd CSS animation to items after they move: in the drop handler, add item.classList.add("just-moved") and remove it after 300ms. Style .just-moved with a brief background flash: background: rgba(99,102,241,0.1); transition: background 0.3s.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with state array reordering, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Task and project priority reordering
Let users drag tasks to reorder by priority. The current order maps to the priority index — position 0 is highest priority. POST the reordered IDs array after each drop to persist priority to your backend without requiring an explicit save button.
Dashboard widget and column arrangement
Allow users to reorder their dashboard widgets. Each draggable item is a widget card. The saved order is stored per user account. Load the saved order on next visit to restore the user's custom arrangement.
Navigation menu and sidebar link reordering
Let users customise the order of pinned links, bookmarks, or sidebar nav items in a settings panel. The reordered array is saved to localStorage or a user preferences API and applied on next load.
Form field and survey question ordering
Form builders and survey tools need drag-to-reorder for questions. Each .sort-item is a question card. The serialised order defines the question display sequence. This is one of the most common use cases for HTML5 drag-and-drop.
Study HTML5 Drag and Drop API without SortableJS
SortableJS is 20KB+ for a use case this snippet handles in under 50 lines. The snippet teaches the five events (dragstart, dragover, dragleave, drop, dragend), the e.preventDefault() requirement, and the DOM insertBefore pattern for reordering.
Playlist and media queue reordering
A music player lets users reorder their queue by dragging. Each .sort-item is a track card with album art, title, and duration. The drag handle icon communicates reorderability. The new order is sent to the playback engine after each drop.

Got questions?

Frequently Asked Questions

By default, HTML elements are not valid drop targets — dropping anything on them cancels the drag. Calling e.preventDefault() inside dragover signals to the browser "this element accepts drops". Without it, the dragover event fires but the drop event never fires. This is the single most common reason drag-and-drop implementations fail: missing e.preventDefault() in dragover.

After each drop, collect the current item IDs: const ids = [...list.querySelectorAll(".sort-item")].map(el => el.dataset.id). POST to your API: fetch("/api/reorder", { method: "PATCH", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ ids }) }). On the server, update the order column for each item to match its array index. Return 200 on success. Call this in showOrder() or in a debounced version to avoid too many requests during rapid reordering.

The HTML5 Drag and Drop API does not fire on touch screens. For touch support: listen to touchstart (record initial y), touchmove (translate the item with touch y offset, find the element under touch using document.elementFromPoint()), touchend (insert before/after the element under the final touch point). Alternatively, use the pointer events API: pointerdown, pointermove, pointerup with pointercapture — more reliable across devices. SortableJS handles this automatically if you prefer a library.

Click "JSX" to download. Manage items as an array in useState. On drop, compute the new order: const newItems = [...items]; const src = newItems.splice(dragSrcIndex, 1)[0]; newItems.splice(dropTargetIndex, 0, src); setItems(newItems). Track dragSrcIndex with useRef. Derive drop position (before/after) from the mouse Y vs target midpoint. Render the array to list items with a key prop set to item.id for correct React reconciliation during reorder.