webdevpuneet.com

Double-Tap to Like — Free Instagram-Style Tap Gesture Snippet

Double-Tap to Like · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="dtl-wrap">
  <article class="dtl-card" id="dtlCard">
    <div class="dtl-media" id="dtlMedia">
      <img class="dtl-img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=700&q=70&auto=format&fit=crop" alt="Forest path" draggable="false">
    </div>
    <div class="dtl-footer">
      <button type="button" class="dtl-like-btn" id="dtlLikeBtn" aria-pressed="false">
        <svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
        <span id="dtlCount">2,481</span>
      </button>
      <span class="dtl-tip">Double-tap the photo to like</span>
    </div>
  </article>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real tap-timing detection
Compares timestamps by hand, not the dblclick event.
Position-aware pairing
Math.hypot rejects taps that land too far apart.
Tap-point-accurate heart
Pops exactly where the second tap landed.
WAAPI fire-and-forget
The heart animates and self-removes on finish.
Idempotent like state
Repeated double-taps never double-count a like.
Independent like button
A normal tap toggles the same underlying state.
Touch and mouse paths
Works via touchend on mobile, click for desktop testing.
No dependency
Pure HTML/CSS/JS — no gesture library.

About this UI Snippet

Double-Tap to Like — Real Gesture Detection, Not dblclick

Screenshot of the Double-Tap to Like snippet rendered live

This is the Instagram-style interaction where tapping a photo twice in quick succession pops a heart from the exact tap location and likes the post, distinct from tapping a dedicated like button (see like burst button for that click-triggered version). The whole point of this snippet is that it detects a genuine double-tap gesture by hand — it does not rely on the browser's native dblclick event, which behaves inconsistently on touch devices and does not report where the second tap landed relative to the first.

Why dblclick is not enough

dblclick fires from two click events close together at the OS/browser level, but on touch devices tap-to-click synthesis is inconsistent across browsers, and dblclick gives you no way to check *where* each tap landed — a double-tap on one corner of a photo and a double-tap on the opposite corner both just fire the same event. This snippet instead listens for touchend (and click, for desktop testing) directly and does the comparison itself.

Timestamp and distance comparison

Every tap calls handleTap(x, y), which computes dt = now - lastTapTime and the on-screen distance from the previous tap with Math.hypot. If the new tap arrives within DOUBLE_TAP_MS (320ms) *and* within DOUBLE_TAP_DIST (36px) of the previous one, it counts as a double-tap and triggers the like; otherwise the tap is simply recorded as the new "last tap" to compare the next one against. Requiring both a time window and a position window is what stops two unrelated taps in different corners of the photo, or two taps a second apart, from being misread as one gesture.

The heart pops from the actual tap point

popHeartAt(x, y) converts the tap's viewport coordinates into a position relative to the photo with getBoundingClientRect(), then creates a heart element positioned exactly there and animates it through a scale-up-with-rotation-then-fade sequence via the Web Animations API, removing itself on onfinish — the same fire-and-forget cleanup pattern as like burst button, but positioned at wherever you actually tapped rather than a fixed spot.

Persistent state, separate from the animation

The heart animation and the liked state are deliberately decoupled: setLiked(true) always runs on a successful double-tap (toggling the button, count, and card styling), but the like button itself can also toggle the same state independently via a normal click, and repeated double-taps on an already-liked photo simply keep the state at "liked" without double-counting, matching real apps where a double-tap only ever *adds* a like visually.

Customizing it

Tune DOUBLE_TAP_MS and DOUBLE_TAP_DIST for stricter or looser detection, change the heart's easing and duration, or spawn multiple hearts per tap for a bigger celebration. Pair it with swipe cards for a full feed-card interaction set.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA photo card renders with a like button beneath it.
  2. 2
    Tap or click the photo onceNothing happens yet — it is recorded as the first tap.
  3. 3
    Tap again quickly, nearbyA heart pops from that exact spot and the post is liked.
  4. 4
    Tap slowly or far apartTwo taps outside the time or distance window do not trigger a like.
  5. 5
    Use the like button directlyIt toggles the same state independent of double-tapping.
  6. 6
    Tune the thresholdsChange DOUBLE_TAP_MS and DOUBLE_TAP_DIST for sensitivity.

Real-world uses

Common Use Cases

Social feed photos
Reproduce the classic double-tap to like on a photo or post, popping a heart from the exact point of the second tap.
Story and reel viewers
Let people like without covering the media with a button, comparing timestamps by hand because dblclick is unreliable on touch.
Photo gallery gestures
Add a like gesture next to a swipe cards deck, rejecting taps that land too far apart using Math.hypot.
Message photo reactions
Let users double-tap a shared photo to react quickly, with a persistent like state toggled alongside the animation.
Button-triggered comparison
Contrast with the click-driven like burst button, as the heart here animates via the Web Animations API and removes itself.
Related: GSAP Flip Layout Transition
See the GSAP Flip Layout Transition for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

dblclick fires from two click events close together, but touch-to-click synthesis is inconsistent across mobile browsers, and dblclick never tells you where each tap actually landed. This snippet listens to touchend and click directly, records each tap's timestamp and coordinates, and compares the new tap against the previous one itself — giving full control over both the timing and position thresholds that count as a double-tap.

handleTap(x, y) computes the time since the previous tap and the on-screen distance between the two tap points using Math.hypot. Only when the new tap arrives within DOUBLE_TAP_MS (320ms) of the previous one, and within DOUBLE_TAP_DIST (36px) of it, does it count as a double-tap and trigger the like. Requiring both conditions stops two unrelated taps in different spots, or two taps a second apart, from being misread as one gesture.

popHeartAt(x, y) takes the tap's viewport coordinates and converts them into a position relative to the photo using getBoundingClientRect(), then positions a new heart element at that exact offset before animating it. Because the coordinates come from the real tap event rather than a fixed center point, double-tapping any corner of the photo pops the heart from that corner.

No. setLiked(true) is what a successful double-tap always calls, regardless of the current state, so repeated double-taps on an already-liked photo simply keep it liked without incrementing the count again. Only the separate like button click toggles the state in both directions, matching how double-tap-to-like behaves in real apps.

Keep lastTapTime/lastTapX/lastTapY in refs (not state, since they change on every tap without needing a re-render), and keep liked/count in component state. Attach the touchend and click listeners to the media ref in a mount effect, call your like API from inside setLiked, and spawn the heart animation with element.animate directly on a DOM node rather than through render.