Long Press

롱 프레스

Press and hold past a threshold to surface an extra menu or action — deliberately distinct from a quick tap.

Also known as: Press and hold
···
html
<div class="lng">
  <button class="lbtn" id="lbtn"><span class="fill" id="lfill"></span><span class="txt">Hold</span></button>
  <div class="menu" id="lmenu">Context menu</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}
.lng{position:absolute;inset:0;display:grid;place-items:center}
.lbtn{position:relative;overflow:hidden;padding:0;width:clamp(84px,30vmin,120px);height:clamp(84px,30vmin,120px);
  border-radius:50%;border:2px solid var(--line);background:var(--surface);cursor:pointer;display:grid;place-items:center}
.fill{position:absolute;left:0;bottom:0;width:100%;height:0%;background:color-mix(in srgb,var(--accent) 70%,transparent)}
.txt{position:relative;font-weight:700;color:var(--fg);font-size:clamp(11px,3.4vmin,13px);z-index:1}
.menu{position:absolute;bottom:8%;padding:8px 14px;border-radius:10px;background:var(--accent);color:#fff;
  font-size:clamp(10px,3vmin,12px);font-weight:700;opacity:0;transform:translateY(6px);transition:opacity .2s,transform .2s}
.menu.show{opacity:1;transform:translateY(0)}
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 btn=document.getElementById('lbtn');
const fill=document.getElementById('lfill');
const menu=document.getElementById('lmenu');
const HOLD_MS=1000;
let raf=null;
function tick(start){
  const p=Math.min(1,(performance.now()-start)/HOLD_MS);
  fill.style.height=(p*100)+'%';
  if(p<1) raf=requestAnimationFrame(()=>tick(start));
  else complete();
}
function startPress(){ raf=requestAnimationFrame(()=>tick(performance.now())); }
function cancelPress(){ if(raf) cancelAnimationFrame(raf); raf=null; fill.style.height='0%'; }
function complete(){
  raf=null; fill.style.height='100%'; menu.classList.add('show');
  setTimeout(()=>{ menu.classList.remove('show'); fill.style.height='0%'; },900);
}
btn.addEventListener('pointerdown',()=>{ fxAuto=false; fxHide(); startPress(); });
addEventListener('pointerup',()=>{ if(raf) cancelPress(); });
addEventListener('pointercancel',()=>{ if(raf) cancelPress(); });
async function autoSeq(){
  while(fxAuto){
    const r=btn.getBoundingClientRect();
    await fxWalk(r.left+r.width/2, r.top+r.height/2, 500); if(!fxAuto) break;
    startPress();
    await fxWait(HOLD_MS+150); if(!fxAuto) break;
    await fxWait(700); if(!fxAuto) break;
    await fxWalk(innerWidth*(0.2+Math.random()*0.6), innerHeight*(0.2+Math.random()*0.6), 400);
    await fxWait(300);
  }
}
autoSeq();

On `pointerdown`, record the start time and use `requestAnimationFrame` to track elapsed time while filling a visual progress indicator (a ring or a fill). Once progress hits 100% (typically 500–1000ms), open the menu.

The cancellation logic is the important part: `pointerup`, `pointercancel`, or moving past a small distance threshold while held (which usually means the user is scrolling, not pressing) must immediately stop the timer and reset progress to zero — otherwise a user trying to scroll gets an unwanted menu.

On touch, this collides with the browser's own long-press (text selection, context menu), so suppress the default with `touch-action: none` or `-webkit-touch-callout: none`.

When to use

Good for hiding secondary actions — a reaction picker in a chat app, entering icon-edit mode. Don't make it the only way in; discoverability is low, so pair it with another path.