G GAMESRICH
Kids & Toddlers ยท Intermediate

How to Build an Interactive Drag and Drop Game: Padan & Letak

Implement smooth multi-touch dragging with setPointerCapture and target collision detection.

By GamesRich Engineering 7 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME โ†—

Architecture & Core Concept

The HTML5 Drag and Drop API has inconsistent mobile support. Pointer Events (pointerdown, pointermove, pointerup) offer 100% reliable cross-device dragging. When a draggable piece is pressed: 1. el.setPointerCapture(e.pointerId) locks touch tracking to the element 2. Track offset: dx = e.clientX - rect.left, dy = e.clientY - rect.top 3. On pointermove, update style.left and style.top 4. Check if pointer is inside any target bounding box using getBoundingClientRect() 5. On drop, verify whether data-key matches. If yes, snap into place!

Key Game Mechanics Taught:

Step-by-Step Implementation

1

Initialize Dragging with Pointer Events

Using setPointerCapture guarantees pointermove events will continue firing even if a fast swipe leaves the element boundaries.

Step 1 Implementation
let activeDrag = null;

function startDrag(e) {
  const el = e.currentTarget;
  if (el.classList.contains('matched')) return;
  e.preventDefault();

  const rect = el.getBoundingClientRect();
  activeDrag = {
    el,
    key: el.dataset.key,
    offsetX: e.clientX - rect.left,
    offsetY: e.clientY - rect.top
  };

  el.classList.add('dragging');
  el.setPointerCapture?.(e.pointerId);

  el.addEventListener('pointermove', onDragMove);
  el.addEventListener('pointerup', onDragEnd, { once: true });
}
2

Check Target Collision in Real-Time

On every pointermove, inspect target elements to highlight the one currently hovered underneath the finger.

Step 2 Implementation
function isInside(x, y, rect) {
  return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}

function onDragMove(e) {
  if (!activeDrag) return;
  activeDrag.el.style.left = (e.clientX - activeDrag.offsetX) + 'px';
  activeDrag.el.style.top = (e.clientY - activeDrag.offsetY) + 'px';

  document.querySelectorAll('.target').forEach(target => {
    const isHovered = isInside(e.clientX, e.clientY, target.getBoundingClientRect());
    target.classList.toggle('highlighted', isHovered);
  });
}
3

Validate Drop & Error Shake Animation

If dropped on the wrong target, play a quick keyframe shake animation and reset the piece to its original position.

Step 3 Implementation
function onDragEnd(e) {
  if (!activeDrag) return;
  const { el, key } = activeDrag;

  const validTarget = [...document.querySelectorAll('.target')].find(t => 
    isInside(e.clientX, e.clientY, t.getBoundingClientRect())
  );

  if (validTarget && validTarget.dataset.key === key) {
    el.classList.add('matched');
    validTarget.classList.add('done');
    playSuccessSound();
  } else {
    // Wrong target: shake and return
    el.animate([
      { transform: 'translateX(-8px)' },
      { transform: 'translateX(8px)' },
      { transform: 'none' }
    ], { duration: 260 });
  }
  activeDrag = null;
}

Complete Standalone Source Code

Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:

Complete Runnable Game (index.html)
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Drag & Match Demo</title>
<style>
body { margin:0; background:#fdf4ff; display:flex; flex-direction:column; align-items:center; padding:30px; font-family:sans-serif; }
.container { display:flex; justify-content:space-between; width:500px; margin-top:40px; }
.pieces, .targets { display:flex; flex-direction:column; gap:16px; }
.piece { width:80px; height:80px; background:#a855f7; color:#fff; font-size:36px; display:flex; align-items:center; justify-content:center; border-radius:12px; cursor:grab; touch-action:none; user-select:none; }
.piece.dragging { position:fixed; pointer-events:none; z-index:999; }
.piece.matched { opacity:0.3; cursor:default; }
.target { width:120px; height:80px; border:3px dashed #d8b4fe; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:20px; color:#6b21a8; font-weight:bold; }
.target.highlighted { border-color:#a855f7; background:#fae8ff; }
.target.done { border-style:solid; background:#f3e8ff; }
</style></head><body>
<h2>Padan & Letak: Shape Match</h2>
<div class="container">
  <div class="pieces">
    <div class="piece" data-key="cat">๐Ÿฑ</div>
    <div class="piece" data-key="bird">๐Ÿฆ</div>
    <div class="piece" data-key="panda">๐Ÿผ</div>
  </div>
  <div class="targets">
    <div class="target" data-key="bird">BIRD</div>
    <div class="target" data-key="cat">CAT</div>
    <div class="target" data-key="panda">PANDA</div>
  </div>
</div>
<script>
let drag = null;
document.querySelectorAll('.piece').forEach(p => {
  p.onpointerdown = e => {
    if(p.classList.contains('matched')) return;
    const r = p.getBoundingClientRect();
    drag = { el: p, key: p.dataset.key, dx: e.clientX - r.left, dy: e.clientY - r.top };
    p.classList.add('dragging');
    p.setPointerCapture(e.pointerId);
    p.onpointermove = ev => {
      p.style.left = (ev.clientX - drag.dx) + 'px';
      p.style.top = (ev.clientY - drag.dy) + 'px';
      document.querySelectorAll('.target').forEach(t => {
        const tr = t.getBoundingClientRect();
        t.classList.toggle('highlighted', ev.clientX >= tr.left && ev.clientX <= tr.right && ev.clientY >= tr.top && ev.clientY <= tr.bottom);
      });
    };
    p.onpointerup = ev => {
      p.onpointermove = null; p.classList.remove('dragging');
      p.style.left = p.style.top = '';
      const t = [...document.querySelectorAll('.target')].find(target => {
        const tr = target.getBoundingClientRect();
        return ev.clientX >= tr.left && ev.clientX <= tr.right && ev.clientY >= tr.top && ev.clientY <= tr.bottom;
      });
      document.querySelectorAll('.target').forEach(target => target.classList.remove('highlighted'));
      if(t && t.dataset.key === drag.key) {
        p.classList.add('matched'); t.classList.add('done');
      }
      drag = null;
    };
  };
});
</script>
</body></html>

Ideas to Expand Your Game

Now that you have the core mechanics running, consider adding: