Double-Tap to Like

더블탭 좋아요

Tap a photo twice quickly and a heart bursts open while it gets liked — the gesture Instagram made ubiquitous.

Also known as: Double tap to like
···
html
<div class="dtl" id="dtlcard">
  <div class="photo"></div>
  <div class="heart" id="heart">&#9829;</div>
  <button class="likebtn" id="likebtn">&#9825; <span id="likecount">128</span></button>
</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}
.dtl{position:relative;width:min(70%,220px);aspect-ratio:4/3;border-radius:14px;overflow:hidden;
  box-shadow:0 12px 26px rgba(0,0,0,.2)}
.photo{position:absolute;inset:0;background:linear-gradient(135deg,var(--accent-3),var(--accent))}
.heart{position:absolute;left:50%;top:50%;font-size:0;color:#fff;transform:translate(-50%,-50%) scale(.4);
  opacity:0;pointer-events:none;text-shadow:0 4px 14px rgba(0,0,0,.35)}
.heart.pop{font-size:56px;animation:heartpop .7s ease forwards}
@keyframes heartpop{0%{opacity:0;transform:translate(-50%,-50%) scale(.3)}30%{opacity:1;transform:translate(-50%,-50%) scale(1.15)}60%{transform:translate(-50%,-50%) scale(.95)}100%{opacity:0;transform:translate(-50%,-50%) scale(1)}}
.likebtn{position:absolute;left:10px;bottom:10px;padding:6px 10px;border:none;border-radius:999px;
  background:rgba(0,0,0,.35);color:#fff;font-size:clamp(11px,3vmin,13px);font-weight:700;display:flex;gap:6px;align-items:center}
.likebtn.liked{color:#ff5c8a}
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('dtlcard');
const heart=document.getElementById('heart');
const likebtn=document.getElementById('likebtn');
const countEl=document.getElementById('likecount');
let liked=false, count=128;
function doLike(){
  heart.classList.remove('pop'); void heart.offsetWidth; heart.classList.add('pop');
  if(!liked){ liked=true; count++; likebtn.classList.add('liked'); likebtn.firstChild.textContent='♥ '; countEl.textContent=String(count); }
}
function resetLike(){
  liked=false; count=128; likebtn.classList.remove('liked'); likebtn.firstChild.textContent='♡ '; countEl.textContent='128';
}
let lastTap=0;
card.addEventListener('pointerdown',()=>{
  fxAuto=false; fxHide();
  const now=performance.now();
  if(now-lastTap<350) doLike();
  lastTap=now;
});
async function autoSeq(){
  while(fxAuto){
    const r=card.getBoundingClientRect();
    await fxWalk(r.left+r.width/2, r.top+r.height/2, 500); if(!fxAuto) break;
    for(let i=0;i<2 && fxAuto;i++){
      fxDot.style.opacity='0.3'; await fxWait(90);
      fxDot.style.opacity='1'; await fxWait(90);
    }
    if(!fxAuto) break;
    doLike();
    await fxWait(1500); if(!fxAuto) break;
    resetLike();
  }
}
autoSeq();

On every `pointerdown`, compare the current time to the previous tap's time; within 350ms counts as a double-tap. Too short a window and it misses real double-taps; too long and two unrelated taps accidentally register as a like.

Once recognized, bounce a heart open in the center — `scale(0.3) → scale(1.15) → scale(1)` — while fading `opacity` to zero, all in one `@keyframes` pass. If it's already liked, most social apps don't unlike it; they just replay the heart animation.

If a single tap needs to do something else (like opening the photo), you can't react on the first tap alone — wait out the 350ms window, and only treat it as a plain single tap if no second tap arrived in time.

When to use

Use it as a fast reaction over photos in a social feed. It's a discoverable-later gesture, so keep an explicit like button alongside it.