Drag and Drop

드래그 앤 드롭

The most basic "pick it up, move it" interaction. Implemented with pointer events, it behaves the same across browsers.

Also known as: DnD
···
html
<div class="dnd">
  <div class="chip" id="chip">&#128230;</div>
  <div class="zone" id="zone">Drop here</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}
.dnd{position:absolute;inset:0}
.chip{position:absolute;width:56px;height:56px;border-radius:14px;background:var(--accent);color:#fff;
  display:grid;place-items:center;font-size:22px;box-shadow:0 10px 20px rgba(0,0,0,.25);cursor:grab;touch-action:none}
.chip.lift{box-shadow:0 18px 30px rgba(0,0,0,.35);transform:scale(1.06)}
.zone{position:absolute;right:8%;top:50%;transform:translateY(-50%);width:84px;height:84px;border-radius:16px;
  border:2px dashed var(--line);display:grid;place-items:center;color:var(--muted);font-size:clamp(10px,3vmin,12px);
  text-align:center;padding:6px;transition:border-color .2s,background .2s,color .2s}
.zone.over{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)}
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 chip=document.getElementById('chip');
const zone=document.getElementById('zone');
let dragOffset=null, dragging=false;
function place(x,y){ chip.style.left=x+'px'; chip.style.top=y+'px'; }
function homeX(){ return innerWidth*0.18; }
function homeY(){ return innerHeight/2-28; }
place(homeX(),homeY());
function overlap(){
  const c=chip.getBoundingClientRect(), z=zone.getBoundingClientRect();
  return !(c.right<z.left||c.left>z.right||c.bottom<z.top||c.top>z.bottom);
}
chip.addEventListener('pointerdown',e=>{
  fxAuto=false; fxHide();
  dragging=true; chip.classList.add('lift'); chip.setPointerCapture(e.pointerId);
  const r=chip.getBoundingClientRect();
  dragOffset={x:e.clientX-r.left,y:e.clientY-r.top};
});
addEventListener('pointermove',e=>{
  if(!dragging) return;
  place(e.clientX-dragOffset.x, e.clientY-dragOffset.y);
  zone.classList.toggle('over', overlap());
});
addEventListener('pointerup',()=>{
  if(!dragging) return;
  dragging=false; chip.classList.remove('lift');
  if(overlap()){
    const z=zone.getBoundingClientRect();
    place(z.left+z.width/2-28, z.top+z.height/2-28);
  } else place(homeX(),homeY());
  zone.classList.remove('over');
});
async function autoSeq(){
  while(fxAuto){
    place(homeX(),homeY());
    chip.classList.remove('lift'); zone.classList.remove('over');
    const startR=chip.getBoundingClientRect();
    fxX=startR.left+28; fxY=startR.top+28; fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
    await fxWait(500); if(!fxAuto) break;
    chip.classList.add('lift');
    const zoneR=zone.getBoundingClientRect();
    const tx=zoneR.left+zoneR.width/2, ty=zoneR.top+zoneR.height/2;
    const steps=24;
    for(let i=1;i<=steps;i++){
      if(!fxAuto) break;
      const p=i/steps, e=1-Math.pow(1-p,3);
      const cx=startR.left+28+(tx-(startR.left+28))*e;
      const cy=startR.top+28+(ty-(startR.top+28))*e;
      fxX=cx; fxY=cy; fxDot.style.transform='translate('+cx+'px,'+cy+'px)';
      place(cx-28, cy-28);
      zone.classList.toggle('over', overlap());
      await new Promise(res=>requestAnimationFrame(res));
    }
    if(!fxAuto) break;
    chip.classList.remove('lift');
    place(tx-28, ty-28);
    zone.classList.remove('over');
    await fxWait(1000);
  }
}
autoSeq();

On `pointerdown`, record the offset between the grabbed element and the cursor; on every `pointermove`, update its `left/top` (or `transform`). Calling `setPointerCapture` keeps `pointermove` events coming even if the cursor darts outside the element.

Each frame, check whether the dragged element's and drop zone's `getBoundingClientRect()` overlap, and give the zone visual feedback (a highlighted border) when they do. On `pointerup`, snap into the zone's center if overlapping, or return to the start position otherwise.

The native HTML Drag and Drop API (`draggable`, `dragstart`, etc.) exists too, but it's hard to control the drag preview precisely and it doesn't support mobile touch — which is why most real implementations build it from pointer events directly.

When to use

Use it for kanban boards, file-upload zones, and editable ordered lists. Always make droppable areas visually distinct.