Drag to Reorder

드래그로 순서 바꾸기

Drag a list item up or down to reorder it — the other items slide out of the way with an animated transition.

Also known as: Sortable listReorderable list
···
html
<div class="reo" id="reo">
  <div class="ritem">Item 1 <span class="grip">&#10241;</span></div>
  <div class="ritem">Item 2 <span class="grip">&#10241;</span></div>
  <div class="ritem">Item 3 <span class="grip">&#10241;</span></div>
</div>
css
.fx-dot{position:absolute;left:0;top:0;width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 18%,transparent),0 2px 6px rgba(0,0,0,.3);
  pointer-events:none;z-index:9999;transition:opacity .25s}
.reo{position:relative;width:min(86%,260px);height:132px}
.ritem{position:absolute;left:0;right:0;height:36px;display:flex;align-items:center;justify-content:space-between;
  padding:0 14px;border-radius:10px;background:var(--surface);border:1px solid var(--line);color:var(--fg);
  font-size:clamp(11px,3.2vmin,13px);font-weight:600;cursor:grab;user-select:none;touch-action:none;
  transition:top .18s ease,box-shadow .18s;box-shadow:0 1px 2px rgba(0,0,0,.06)}
.ritem.drag{cursor:grabbing;box-shadow:0 12px 22px rgba(0,0,0,.22);z-index:5}
.grip{opacity:.4;font-size:14px}
js
const fxDot=document.createElement('div');fxDot.className='fx-dot';document.body.appendChild(fxDot);
let fxAuto=true,fxX=innerWidth/2,fxY=innerHeight/2;
function fxHide(){fxDot.style.opacity='0';}
addEventListener('pointerdown',()=>{fxAuto=false;fxHide();},{capture:true});
addEventListener('pointermove',e=>{fxAuto=false;fxHide();fxX=e.clientX;fxY=e.clientY;},{capture:true});
function fxWalk(x,y,ms){
  return new Promise(res=>{
    const sx=fxX,sy=fxY,start=performance.now();
    function step(now){
      if(!fxAuto)return res();
      const p=Math.min(1,(now-start)/ms),e=1-Math.pow(1-p,3);
      fxX=sx+(x-sx)*e;fxY=sy+(y-sy)*e;
      fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      if(p<1)requestAnimationFrame(step);else res();
    }
    requestAnimationFrame(step);
  });
}
function fxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!fxAuto)return res(); if(now-start<ms)requestAnimationFrame(step);else res(); }
    requestAnimationFrame(step);
  });
}

const reo=document.getElementById('reo');
const items=[...reo.querySelectorAll('.ritem')];
const H=44;
let order=[0,1,2];
function layout(){ order.forEach((idx,slot)=>{ items[idx].style.top=(slot*H)+'px'; }); }
layout();
let dragIdx=null, dragStartY=0, dragStartTop=0;
function beginDrag(idx, clientY){
  dragIdx=idx;
  const el=items[idx];
  el.classList.add('drag'); el.style.transition='none';
  dragStartY=clientY; dragStartTop=order.indexOf(idx)*H;
}
function dragTo(clientY){
  if(dragIdx===null) return;
  const el=items[dragIdx];
  const top=dragStartTop+(clientY-dragStartY);
  el.style.top=top+'px';
  const slot=Math.min(items.length-1,Math.max(0,Math.round(top/H)));
  const curSlot=order.indexOf(dragIdx);
  if(slot!==curSlot){
    order.splice(curSlot,1);
    order.splice(slot,0,dragIdx);
    order.forEach((idx2,slot2)=>{ if(idx2!==dragIdx){ items[idx2].style.top=(slot2*H)+'px'; } });
  }
}
function endDrag(){
  if(dragIdx===null) return;
  const el=items[dragIdx];
  el.classList.remove('drag'); el.style.transition='top .2s ease';
  el.style.top=(order.indexOf(dragIdx)*H)+'px';
  dragIdx=null;
}
items.forEach((el,idx)=>{
  el.addEventListener('pointerdown',e=>{ fxAuto=false; fxHide(); beginDrag(idx,e.clientY); });
});
addEventListener('pointermove',e=>dragTo(e.clientY));
addEventListener('pointerup',endDrag);
async function autoSeq(){
  while(fxAuto){
    const r=reo.getBoundingClientRect();
    const idx=order[0];
    const startY=r.top+18;
    fxX=r.left+r.width/2; fxY=startY; fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
    await fxWait(500); if(!fxAuto) break;
    beginDrag(idx, startY);
    const total=(items.length-1)*H;
    const n=24;
    for(let i=1;i<=n;i++){
      if(!fxAuto) break;
      const y=startY+total*(i/n);
      dragTo(y);
      fxY=y; fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      await new Promise(res=>requestAnimationFrame(res));
    }
    if(!fxAuto) break;
    endDrag();
    await fxWait(1000);
  }
}
autoSeq();

Track each item's on-screen slot (order) in an array, and compute `top` as slot number times slot height. Turn off `transition` only on the item being dragged so it tracks the finger exactly; the rest use `transition: top .2s` to slide smoothly into their new spot.

Every frame while dragging, divide the current `top` by the slot height and round it to know which slot you're hovering over. The moment that differs from the item's original slot, splice it into its new array position and animate only the items that need to make room.

On `pointerup`, snap the dragged item exactly into its final slot. On mobile, adding a short "grip" delay before a hold turns into a drag reduces conflicts with scrolling.

When to use

Use it for playlists, task priority, dashboard widget layout — any list where the order itself carries meaning.