Drag-and-Drop Sortable List

Build interactive sortable lists with drag-and-drop
Learn how to create draggable list items that can be reordered using hyperscript's drag-and-drop events. Perfect for task lists, priority queues, and custom ordering!
draggable dragstart dragover drop

Interactive Task List

Drag items to reorder them, or use the controls to manage the list:

The Code

Event Delegation Pattern (Container-Level Handlers):

<!-- Visual insertion marker --> <div id="insert-marker"></div> <!-- All drag handlers on the container, not each item --> <ul id="task-list" _="on dragstart set item to target.closest('.sortable-item') if item is not null then add .dragging to item end set the event.dataTransfer.effectAllowed to 'move' on dragend set dragged to me.querySelector('.dragging') if dragged is not null then remove .dragging from dragged end add { opacity: 0; } to #insert-marker on dragover halt the event set dropTarget to target.closest('.sortable-item') if dropTarget is null then exit end if dropTarget matches .dragging then exit end -- Position insertion marker set box to dropTarget.getBoundingClientRect() set midpoint to box.top + box.height / 2 if event.clientY < midpoint then set markerTop to box.top else set markerTop to box.bottom end add { top: ${markerTop}px; left: ${box.left}px; width: ${box.width}px; opacity: 1; } to #insert-marker on drop halt the event add { opacity: 0; } to #insert-marker set dropTarget to target.closest('.sortable-item') set draggedItem to me.querySelector('.dragging') if dropTarget is null or draggedItem is null then exit end set box to dropTarget.getBoundingClientRect() if event.clientY < box.top + box.height / 2 then call me.insertBefore(draggedItem, dropTarget) else call me.insertBefore(draggedItem, dropTarget.nextSibling) end"> <!-- Items are simple - no individual handlers needed --> <li class="sortable-item" draggable="true"> <span class="drag-handle">☰</span> <div class="item-content">Task content</div> </li> </ul>

Delete Item:

<button _="on click remove closest .sortable-item"> Delete </button>

Reverse List Order:

<button _="on click call #task-list.children.reverse() for item in #task-list.children call #task-list.appendChild(item) end"> Reverse </button>

How it works:

Key Concepts:

Drag Event Sequence:

  1. dragstart: User starts dragging an item
  2. dragover: Dragged item moves over a drop target
  3. dragleave: Dragged item leaves a drop target
  4. drop: User releases the item over a drop target
  5. dragend: Drag operation completes (success or cancel)

Advanced Patterns:

<!-- Drag between multiple lists --> <ul class="droppable-list" _="on drop halt the event get first .dragging if result exists put result at end of me end"> </ul> <!-- Save order to localStorage --> <button _="on click set order to [] for item in .sortable-item call order.push(item.id) end call localStorage.setItem('order', JSON.stringify(order))"> Save Order </button> <!-- Restore from localStorage --> <div _="on load set saved to localStorage.getItem('order') if saved set order to JSON.parse(saved) for id in order get #{id} if result then put result at end of #list end end end"> </div> <!-- Drag handles only --> <li class="sortable-item"> <span class="drag-handle" draggable="true" _="on dragstart set the dragTarget to closest .sortable-item add .dragging to dragTarget"> ☰ </span> <div>Content (not draggable)</div> </li>

Common Use Cases:

Accessibility Considerations:

Try it yourself: