Swipe to Dismiss

스와이프로 닫기

Swipe a card or list item sideways to remove it — past a threshold it flies off screen, short of it it springs back.

Also known as: Swipe to delete
···
html
<div class="swp">
  <div class="hint left">&#10005; delete</div>
  <div class="hint right">&#10003; done</div>
  <div class="card2" id="swcard">Swipe me <span>&#8596;</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}
.swp{position:absolute;inset:0;display:grid;place-items:center;overflow:hidden}
.card2{position:relative;width:min(70%,220px);padding:22px 18px;border-radius:14px;background:var(--surface);
  border:1px solid var(--line);box-shadow:0 10px 24px rgba(0,0,0,.12);font-weight:600;color:var(--fg);
  display:flex;align-items:center;justify-content:space-between;gap:8px;touch-action:none;cursor:grab;z-index:2}
.card2 span{opacity:.5}
.hint{position:absolute;top:50%;transform:translateY(-50%);font-size:clamp(10px,3vmin,12px);font-weight:700;opacity:0;transition:opacity .2s}
.hint.left{left:8%;color:#e5534b}
.hint.right{right:8%;color:#1a9e6d}
.hint.show{opacity:1}
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 card=document.getElementById('swcard');
const hintL=document.querySelector('.hint.left'), hintR=document.querySelector('.hint.right');
let dragging=false, startX=0, dx=0;
function applyDx(v){
  dx=v;
  card.style.transform='translateX('+v+'px) rotate('+(v/14)+'deg)';
  card.style.opacity=String(Math.max(0.15,1-Math.abs(v)/240));
  hintL.classList.toggle('show', v<-30);
  hintR.classList.toggle('show', v>30);
}
applyDx(0);
card.addEventListener('pointerdown',e=>{
  fxAuto=false; fxHide();
  dragging=true; startX=e.clientX; card.style.transition='none'; card.setPointerCapture(e.pointerId);
});
addEventListener('pointermove',e=>{ if(!dragging) return; applyDx(e.clientX-startX); });
addEventListener('pointerup',()=>{
  if(!dragging) return; dragging=false;
  if(Math.abs(dx)>90){
    const dir=dx>0?1:-1;
    card.style.transition='transform .3s ease,opacity .3s ease';
    card.style.transform='translateX('+(dir*500)+'px) rotate('+(dir*24)+'deg)';
    card.style.opacity='0';
    setTimeout(()=>{ card.style.transition='none'; applyDx(0); card.style.opacity='1'; },500);
  } else { card.style.transition='transform .25s ease,opacity .25s ease'; applyDx(0); }
});
async function autoSeq(){
  while(fxAuto){
    card.style.transition='transform .25s ease,opacity .25s ease'; applyDx(0);
    const r=card.getBoundingClientRect();
    fxX=r.left+r.width/2; fxY=r.top+r.height/2; fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
    await fxWait(500); if(!fxAuto) break;
    const dir=Math.random()>0.5?1:-1;
    card.style.transition='none';
    const steps=20;
    for(let i=1;i<=steps;i++){
      if(!fxAuto) break;
      const v=dir*i*10;
      applyDx(v);
      fxX=r.left+r.width/2+v; fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      await new Promise(res=>requestAnimationFrame(res));
    }
    if(!fxAuto) break;
    card.style.transition='transform .3s ease,opacity .3s ease';
    card.style.transform='translateX('+(dir*500)+'px) rotate('+(dir*24)+'deg)';
    card.style.opacity='0';
    await fxWait(700);
  }
}
autoSeq();

Apply `translateX` for the drag distance (`dx`) along with a slight `rotate` (roughly `dx / 14` degrees) so the card tilts naturally in the direction you're pushing it. As `dx` grows, fade the `opacity` down too, previewing that it's about to disappear.

At `pointerup`, decide whether to commit based on whether `|dx|` crossed a threshold (usually 30–40% of the card's width). Past it, keep flying the card off screen in the same direction; short of it, transition `transform: translateX(0)` back to rest.

If left and right carry different meanings (delete vs. done), reveal a background color or icon hint based on the sign of `dx` while dragging — otherwise users lose track of what crossing the threshold actually does.

When to use

Use it for mobile lists you triage quickly — inbox, to-dos. Pair it with an undo toast to soften accidental swipes.