← Back to Blog Apr 23, 2026 · 06:00 PM · 6 min read

Snake Mobile Controls: Swipe to Change Direction

In Part 4, we added wall wrapping and kids mode. Now we make the game accessible on every device. Most players will find your game on their phone — without touch controls, they cannot play at all. In this part, we add swipe-based direction controls that feel natural on mobile and make the canvas responsive.

By Wuidi · Tutorial · Part 5

What You Will Learn

Step 1: Touch Event Basics

Mobile browsers fire three main touch events: touchstart when a finger touches the screen, touchmove as it slides, and touchend when it lifts. For swipe detection, we only need touchstart and touchend.

let touchStartX = 0;
let touchStartY = 0;

document.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];
  touchStartX = touch.clientX;
  touchStartY = touch.clientY;
}, { passive: true });

document.addEventListener('touchend', (e) => {
  const touch = e.changedTouches[0];
  const deltaX = touch.clientX - touchStartX;
  const deltaY = touch.clientY - touchStartY;

  handleSwipe(deltaX, deltaY);
}, { passive: true });

We record the finger position on touchstart, then calculate how far it moved on touchend. The difference (delta) tells us the swipe direction and distance.

Tip: Use { passive: true } on touch listeners when you do not call preventDefault(). This tells the browser the event will not be cancelled, allowing smoother scrolling performance.

Step 2: Calculate Swipe Direction

Compare the absolute values of deltaX and deltaY to determine if the swipe is horizontal or vertical. Then check the sign to get the exact direction.

function handleSwipe(deltaX, deltaY) {
  const absDx = Math.abs(deltaX);
  const absDy = Math.abs(deltaY);

  if (absDx > absDy) {
    // Horizontal swipe
    if (deltaX > 0) {
      changeDirection('right');
    } else {
      changeDirection('left');
    }
  } else {
    // Vertical swipe
    if (deltaY > 0) {
      changeDirection('down');
    } else {
      changeDirection('up');
    }
  }
}

If the horizontal distance is greater than the vertical distance, it is a horizontal swipe. Otherwise, it is vertical. This simple comparison handles diagonal swipes by picking the dominant axis.

Step 3: Minimum Swipe Threshold

Tiny accidental touches should not change direction. Add a minimum distance threshold — if the finger moved less than 20 pixels, ignore it.

const SWIPE_THRESHOLD = 20; // minimum pixels

function handleSwipe(deltaX, deltaY) {
  const absDx = Math.abs(deltaX);
  const absDy = Math.abs(deltaY);

  // Ignore tiny movements
  if (absDx < SWIPE_THRESHOLD && absDy < SWIPE_THRESHOLD) {
    return;
  }

  if (absDx > absDy) {
    if (deltaX > 0) changeDirection('right');
    else changeDirection('left');
  } else {
    if (deltaY > 0) changeDirection('down');
    else changeDirection('up');
  }
}
Tip: 20 pixels works well on most devices. On high-DPI phones, you might increase it to 30. Too high and the controls feel sluggish; too low and accidental taps register as swipes.

Step 4: Map Swipe to Direction

The changeDirection() function maps swipe names to direction vectors, with the same reversal prevention we use for keyboard input.

function changeDirection(dir) {
  switch (dir) {
    case 'up':
      if (direction.y === 0)
        nextDirection = { x: 0, y: -1 };
      break;
    case 'down':
      if (direction.y === 0)
        nextDirection = { x: 0, y: 1 };
      break;
    case 'left':
      if (direction.x === 0)
        nextDirection = { x: -1, y: 0 };
      break;
    case 'right':
      if (direction.x === 0)
        nextDirection = { x: 1, y: 0 };
      break;
  }
}

This is the same logic as the keyboard handler from Part 1, extracted into a shared function. Both keyboard and touch input call changeDirection(), so the reversal prevention works identically for both.

// Refactor keyboard handler to use the same function
document.addEventListener('keydown', (e) => {
  switch (e.key) {
    case 'ArrowUp':    changeDirection('up');    break;
    case 'ArrowDown':  changeDirection('down');  break;
    case 'ArrowLeft':  changeDirection('left');  break;
    case 'ArrowRight': changeDirection('right'); break;
  }
});

Step 5: Responsive Canvas

On mobile, the canvas needs to fit the screen width. We scale it with CSS while keeping the internal resolution fixed. This gives us sharp rendering at any screen size.

function resizeCanvas() {
  const maxWidth = Math.min(window.innerWidth - 16, 400);
  canvas.style.width = maxWidth + 'px';
  canvas.style.height = maxWidth + 'px';
}

// Resize on load and orientation change
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', resizeCanvas);
resizeCanvas();

The canvas internal size stays at 400×400 (or whatever your grid dictates). CSS width and height scale the display without affecting the coordinate system. This means all your drawing code works unchanged — the browser handles the scaling.

Tip: Subtract 16px from the window width to add 8px padding on each side. This prevents the canvas from touching the screen edges, which looks cleaner and avoids triggering browser edge gestures.

Step 6: Hide UI on Small Screens

Some desktop UI elements do not make sense on mobile. Use CSS media queries to hide or rearrange them.

<style>
  @media (max-width: 600px) {
    .desktop-controls {
      display: none;
    }

    .game-container {
      padding: 4px;
    }

    canvas {
      display: block;
      margin: 0 auto;
      max-width: 100%;
      height: auto;
    }
  }
</style>

Hide keyboard instruction text (like "Use arrow keys") on mobile since it does not apply. Keep the score display visible — players always want to see their score. If you have a restart button, make it large enough to tap easily (at least 44×44 pixels, per Apple's Human Interface Guidelines).

Step 7: Testing on Mobile

Desktop browser DevTools have a device emulator, but it does not catch everything. Here is a practical testing checklist:

// Prevent page scroll while playing
document.addEventListener('touchmove', (e) => {
  if (!gameOver) {
    e.preventDefault();
  }
}, { passive: false });
Tip: Use { passive: false } only on the touchmove listener where you call preventDefault(). This stops the page from scrolling during gameplay but allows normal scrolling when the game is not active.

What's Next?

With mobile controls in place, your Snake game works on every device. In the final part, we add sound effects — eat sounds, death sounds, and level-up jingles using the Web Audio API.

Continue to Part 6: Sound Effects →

More Articles

🔊
Tutorial Snake Sound Effects: Eat, Die, and Level Up Sounds Tutorial · Apr 23, 2026 · 07:00 PM
🐍
Tutorial How to Make a Snake Game: Fundamentals for Beginners Tutorial · Apr 23, 2026 · 02:00 PM
🏆
Tutorial Snake High Score: Save and Display Best Scores Tutorial · Apr 23, 2026 · 03:00 PM
← Back to Blog